内置序列的概述
python 的内置序列分为两种:
- 容器序列。意思就是内部的元素类型可以是不同的,比如 list,tuple,collections.deque
- 平铺序列。意思就是内部的元素类型是相同的,比如 str,bytes,array.array
容器序列内部存储的是对象的引用,所以可以是任何类型。平铺序列直接用其自己的内存空间来存储对象的值,这两个序列类型本质都是一样的,只不过容器内部存的都是地址。
平铺序列更紧凑一些,但是保存的数值也仅限 primitive machine values like bytes, integers, and floats.
另外一种分类序列的规则:
- 可变序列,如:list, bytearray, array.array, collections.deque
- 不可变序列,如:tuple, str, bytes
列表推导和生成器表达式
列表推导在可读性上比创建 list 再插入要好:
>>> symbols = '$¢£¥€¤'
>>> codes = []
>>> for symbol in symbols:
... codes.append(ord(symbol))
...
>>> codes
[36, 162, 163, 165, 8364, 164]
>>> symbols = '$¢£¥€¤'
>>> codes = [ord(symbol) for symbol in symbols]
>>> codes
[36, 162, 163, 165, 8364, 164]
列表推导内变量的作用范围
介绍了一下 Walrus 操作符 :=
看例子应该一目了然
>>> x = 'ABC'
>>> codes = [ord(x) for x in x]
>>> x
'ABC'
>>> codes
[65, 66, 67]
>>> codes = [last := ord(c) for c in x]
>>> last
67
>>> c
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined
之后对比了一下列表推导和 map 与 filter:
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii
[162, 163, 165, 8364, 164]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
>>> beyond_ascii
[162, 163, 165, 8364, 164]
列表推导减少对 lambda 函数的使用,更简单一些
列表推导在计算笛卡尔积的情况:
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]
>>> tshirts
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'),
('white', 'M'), ('white', 'L')]
>>> for color in colors:
... for size in sizes:
... print((color, size))
...
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')
>>> tshirts = [(color, size) for size in sizes
... for color in colors]
>>> tshirts
[('black', 'S'), ('white', 'S'), ('black', 'M'), ('white', 'M'),
('black', 'L'), ('white', 'L')]
主要注意 for 循环在列表推导从左到右的顺序就是正常情况下从从上到下的顺序即可,列表推导返回的是列表
接下来看生成器表达式,它比列表推导节省内存,因为他是一个元素一个元素这么生成的,列表推导是创建一整个列表然后提供给构造器
语法上只是把 中括号 [] 改成了 括号()
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols) 1
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols)) 2
array('I', [36, 162, 163, 165, 8364, 164])
节省内存可以体现在求笛卡尔积的情况(如果两个列都特别长):
>>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> for tshirt in (f'{c} {s}' for c in colors for s in sizes):
... print(tshirt)
...
black S
black M
black L
white S
white M
white L
元组不仅仅是不可变的list
元组可以作为记录,内部元素的顺序很重要,可以用作没有字段的数据结构:
>>> lax_coordinates = (33.9425, -118.408056)
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),
... ('ESP', 'XDA205856')]
>>> for passport in sorted(traveler_ids):
... print('%s/%s' % passport)
...
BRA/CE342567
ESP/XDA205856
USA/31195855
>>> for country, _ in traveler_ids:
... print(country)
...
USA
BRA
ESP
元组作为不可变list
当你看到元组的时候,你就知道它的长度不能改变
元组的性能比 list 好,python 可以对其做优化
元组虽然作为不可变列,但是其内部的元素是可变的,元组内部对于元素的引用没有改变”:
>>> a = (10, 'alpha', [1, 2])
>>> b = (10, 'alpha', [1, 2])
>>> a == b
True
>>> b[-1].append(99)
>>> a == b
False
>>> b
(10, 'alpha', [1, 2, 99])
元组内部的是否有引用可变序列可以通过其是否能被哈希来判断:
>>> def fixed(o):
... try:
... hash(o)
... except TypeError:
... return False
... return True
...
>>> tf = (10, 'alpha', (1, 2))
>>> tm = (10, 'alpha', [1, 2])
>>> fixed(tf)
True
>>> fixed(tm)
False
元组比列有效的原因有大概 4 点:
- 在数据处理上,元组可以用字节码来一次性操作,然而 list 必须一个一个把其中的元素推到栈上
- tuple(t) 返回的是 同一个 t 的引用,list(l) 返回的确实 l 的 复制
- 内存上,元组定长,所以内存大小分配固定
- 列需要不断的重新分配内存来增加空间
在特殊方法的比较上,相比于 list,tuple 缺少添加和删除元素的方法,外加一个 __reversed__,因为 reversed(my_tuple) 不是靠这个特殊方法实现的,因为有优化。
拆包可以避免不必要的索引检索,另外对于任何可迭代的对象都可以拆包,不一定实现 __getitem__ 特殊方法。要求就是迭代器每次迭代生成的一个元素,除非用 * 来捕捉剩余元素。
典型的迭代使用方法:
>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # unpacking
>>> latitude
33.9425
>>> longitude
-118.408056
变量交换:
>>> b, a = a, b
使用 * 来拆包:
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4)
还是同样类似的例子:
>>> a, b, *rest = range(5)
>>> a, b, rest
(0, 1, [2, 3, 4])
>>> a, b, *rest = range(3)
>>> a, b, rest
(0, 1, [2])
>>> a, b, *rest = range(2)
>>> a, b, rest
(0, 1, [])
任何位置也可以:
>>> a, *body, c, d = range(5)
>>> a, body, c, d
(0, [1, 2], 3, 4)
>>> *head, b, c, d = range(5)
>>> head, b, c, d
([0, 1], 2, 3, 4)
函数传参的时候,很典型:
>>> def fun(a, b, c, d, *rest):
... return a, b, c, d, rest
...
>>> fun(*[1, 2], 3, *range(4, 7))
(1, 2, 3, 4, (5, 6))
定义一些序列也可以用到:
>>> *range(4), 4
(0, 1, 2, 3, 4)
>>> [*range(4), 4]
[0, 1, 2, 3, 4]
>>> {*range(4), 4, *(5, 6, 7)}
{0, 1, 2, 3, 4, 5, 6, 7}
之后展示了一个嵌套拆包的用例:
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for name, _, _, (lat, lon) in metro_areas:
if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
if __name__ == '__main__':
main()
对于单独元素的元组拆包后想再生成一个元组:需要再后面加逗号,如:
(record,) 或者 ((record,),)
序列模式匹配
python 3.10 的新特性
之后给了一个机器人命令的例子:
def handle_command(self, message):
match message:
case ['BEEPER', frequency, times]:
self.beep(times, frequency)
case ['NECK', angle]:
self.rotate_neck(angle)
case ['LED', ident, intensity]:
self.leds[ident].set_brightness(ident, intensity)
case ['LED', ident, red, green, blue]:
self.leds[ident].set_color(ident, red, green, blue)
case _:
raise InvalidCommand(message)
和 c 语言的 switch case 很像,不过多了一个析构的步骤,对元组的析构也是可以的:
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
def main():
print(f'{"":15} | {"latitude":>9} | {"longitude":>9}')
for record in metro_areas:
match record:
case [name, _, _, (lat, lon)] if lon <= 0:
print(f'{name:15} | {lat:9.4f} | {lon:9.4f}')
总结来说,序列析构的条件分三点:
- 对象必须是序列
- 元素个数必须匹配
- 元素互相对应
需要注意的是:str,bytes 和 bytearray 不能析构模式匹配,它们被当成一个单独的元素作为匹配的对象。
模式匹配析构时有几个小 tips:
- _ 下划线可以对应忽略的元素
- as 可以重新定义变量名
- 变量前面也可以加类型()作为类型匹配的条件
- case 后面可以加 if 作为判断条件
之后给了一个用模式匹配重构的小函数,可以作为参考:
原始代码:
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
if isinstance(exp, Symbol): # variable reference
return env[exp]
# ... lines omitted
elif exp[0] == 'quote': # (quote exp)
(_, x) = exp
return x
elif exp[0] == 'if': # (if test conseq alt)
(_, test, consequence, alternative) = exp
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
elif exp[0] == 'lambda': # (lambda (parm…) body…)
(_, parms, *body) = exp
return Procedure(parms, body, env)
elif exp[0] == 'define':
(_, name, value_exp) = exp
env[name] = evaluate(value_exp, env)
# ... more lines omitted
重构后:
def evaluate(exp: Expression, env: Environment) -> Any:
"Evaluate an expression in an environment."
match exp:
# ... lines omitted
case ['quote', x]:
return x
case ['if', test, consequence, alternative]:
if evaluate(test, env):
return evaluate(consequence, env)
else:
return evaluate(alternative, env)
case ['lambda', [*parms], *body] if body:
return Procedure(parms, body, env)
case ['define', Symbol() as name, value_exp]:
env[name] = evaluate(value_exp, env)
# ... more lines omitted
case _:
raise SyntaxError(lispstr(exp))
切片
所有的序列都支持的一种特性
解释了为啥最后一个索引没有包括在切片里:
- 很明显看出切片长度,比如:range(3), my_list[:3]
- 很容易计算出切片长度如果给了start 和 stop,用stop - start
- 很容易把序列分成两个切片,如:my_list[:x] 和 my_list[x:]
>>> l = [10, 20, 30, 40, 50, 60]
>>> l[:2] # split at 2
[10, 20]
>>> l[2:]
[30, 40, 50, 60]
>>> l[:3] # split at 3
[10, 20, 30]
>>> l[3:]
[40, 50, 60]
s[a:b:c] 这种形式的切片是以 c 为步长,可以为负值,及为调转的顺序
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
其实内部的 a:b:c 会调用一个内置函数 slice(a,b,c) 然后将结果赋予 seq.__getitem__
多维的切片可以在numpy.ndarray中使用,对于 python 原生的序列类型,除了 memoryview,其他都是一维的
省略号在多维切片的时候可以作为其他的省略参数,例如:x[i,...] = x[i,;,;,;]
对于可变序列的切片同样可以赋值:
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]
+ 和 * 在序列中的作用
>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
注意,+ 和 * 总会创建一个新的对象,不会改变原来的对象
一个经典的创建二维数组的问题:
>>> board = [['_'] * 3 for i in range(3)]
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
>>> weird_board = [['_'] * 3] * 3
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O'
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
这两种方式的不同在于第二种是对列表中的列表的引用进行了复制,所以对引用的改变会触发其他引用这个列的改变,之后也会详细讲这个问题
增量复制的操作对应的特殊函数 __iadd__ 和 __imul__ 全称为 in-place addition/multiplication, 这样就比较容易理解了,最后会有一个赋值的操作:
>>> l = [1, 2, 3]
>>> id(l)
4311953800
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800
>>> t = (1, 2, 3)
>>> id(t)
4312681568
>>> t *= 2
>>> id(t)
4301348296
一个特别经典的题
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
结果其实有两部分:
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
把 t[2] += [50, 60] 分解来看,但看可变序列list是可以增量操作的,但是对元组中的元素也进行了赋值操作,哪怕赋的值还是原来的可变序列引用,也是不行的
对比 list.sort 和 sorted 内置函数
list.sort 是对 list 对象内部进行了修改,没有复制,会返回 None 以告知使用者,相反 sorted 函数会创建一个新的对象。
相同点在于它们都接受相同的可选参数,一个是reverse,另外一个是key,有几个使用例子:
>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry']
>>> fruits
['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple']
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple']
>>> fruits
['grape', 'raspberry', 'apple', 'banana']
>>> fruits.sort()
>>> fruits
['apple', 'banana', 'grape', 'raspberry']
之后介绍了几种情况,当可变列表不是最佳的选择的时候:
第一个是 Arrays,当存储的值是大量的浮点数的时候,Array 比 list 好,并且多便利的保存和读取方法:
>>> from array import array
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))
>>> floats[-1]
0.07802343889111107
>>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp)
>>> fp.close()
>>> floats2 = array('d')
>>> fp = open('floats.bin', 'rb')
>>> floats2.fromfile(fp, 10**7)
>>> fp.close()
>>> floats2[-1]
0.07802343889111107
>>> floats2 == floats
True
另外一种叫memoryview,顾名思义就是能直接通过共享内存来对字节进行操作的:
>>> from array import array
>>> octets = array('B', range(6))
>>> m1 = memoryview(octets)
>>> m1.tolist()
[0, 1, 2, 3, 4, 5]
>>> m2 = m1.cast('B', [2, 3])
>>> m2.tolist()
[[0, 1, 2], [3, 4, 5]]
>>> m3 = m1.cast('B', [3, 2])
>>> m3.tolist()
[[0, 1], [2, 3], [4, 5]]
>>> m2[1,1] = 22
>>> m3[1,1] = 33
>>> octets
array('B', [0, 1, 2, 33, 22, 5])
cast 方法可以改变读取方式,以此来生成新的memoryview引用
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) 1
>>> len(memv)
5
>>> memv[0] 2
-2
>>> memv_oct = memv.cast('B') 3
>>> memv_oct.tolist() 4
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 5
>>> numbers
array('h', [-2, -1, 1024, 1, 2])
在比特位上修改值得操作
numpy不多赘述,特别重要,需要单独学习的
对于频繁的添加以及删除操作的情况,双向队列比 list 好很多,而且是线程安全的:
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
>>> dq.appendleft(-1)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33])
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40])
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
剩下就是总结了