序列构成的数组
此笔记记录于《流畅的 python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为 jupyter 转的 markdown,原始版 jupyter 笔记在这个仓库
Python 也从 ABC 那里继承了用统一的风格去处理序列数据这一特点。不管是哪种数据结构,字符串、列表、字节序列、数组、XML 元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。
内置序列类型概览
序列的分类:
- 容器序列:list、tuple、collections.deque,能存放不同类型的数据
- 扁平序列:str、bytes、bytearray、memoryview、array.array,只能容纳一种类型
重点:容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。
序列类型还可以按照能否被修改来分类:
- 可变序列:list、bytearray、array.array、collections.deque、memoryview
- 不可变序列:tuple、str、bytes
列表推导和生成器表达式
列表推导是一种构建列表的方法,它异常强大,然而由于相关的句法比较晦涩,人们往往不愿意去用它。掌握列表推导还可以为我们打开生成器表达式(generator expression)的大门,后者具有生成各种类型的元素并用它们来填充序列的功能。
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。如果你的代码里并不经常使用它们,那么很可能你错过了许多写出可读性更好且更高效的代码的机会。
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
codes
[36, 162, 163, 165, 8364, 164]
Python 会忽略代码里[]、{}和( )中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符\。
filter 和 map 合起来能做的事情,列表推导也可以做,而且还不需要借助难以理解和阅读的 lambda 表达式:
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]
在 Python 中,lambda
表达式用于创建匿名函数,也就是没有名字的函数。这种函数被称为lambda
函数。lambda
函数的语法如下:
lambda arguments: expression
这里,arguments
是传入lambda
函数的参数,可以是一个或多个。expression
是使用这些参数进行操作的表达式。这个表达式的结果就是此lambda
函数的返回值。
例如,下面是一个lambda
函数,它接受两个参数并返回它们的和:
add = lambda x, y: x + y
print(add(5, 3)) # 输出:8
在这个例子中,x
和y
是参数,x + y
是表达式。
# 使用列表推导式计算笛卡尔积
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')]
# 如果想依照先尺码后颜色的顺序来排列,只需要调整从句的顺序
tshirts = [(color, size) for size in sizes for color in colors]
tshirts
[('black', 'S'),
('white', 'S'),
('black', 'M'),
('white', 'M'),
('black', 'L'),
('white', 'L')]
列表推导的作用只有一个:生成列表。如果想生成其他类型的序列,生成器表达式就派上了用场。
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。这是因为生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。前面那种方式显然能够节省内存。
生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
symbols = '$¢£¥€¤'
# 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来
tuple(ord(symbol) for symbol in symbols)
(36, 162, 163, 165, 8364, 164)
import array
# array.array 构造方法需要两个参数,因此必须加上括号
array.array('I', (ord(symbol) for symbol in symbols))
array('I', [36, 162, 163, 165, 8364, 164])
下方示例:则是利用生成器表达式实现了一个笛卡儿积,用以打印出上文中我们提到过的 T 恤衫的 2 种颜色和 3 种尺码的所有组合。与示例 2-4 不同的是,用到生成器表达式之后,内存里不会留下一个有 6 个组合的列表,因为生成器表达式会在每次 for 循环运行时才生成一个组合。如果要计算两个各有 1000 个元素的列表的笛卡儿积,生成器表达式就可以帮忙省掉运行 for 循环的开销,即一个含有 100 万个元素的列表。
生成器表达式逐个产出元素,从来不会一次性产出一个含有 6 个 T 恤样式的列表
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirts in ('%s %s' % (c, s) for c in colors for s in sizes):
print(tshirts)
black S
black M
black L
white S
white M
white L
元组不仅仅是不可变的列表
元组的特点:
- 元组中的元素不能修改
- 可以用于没有字段名的记录
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。
如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息就变得非常重要了。
lax_coordinates = (33.9425, -118.408056)
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 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
元组拆包:
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用来表示忽略多余的元素,在“用来处理多余的元素”一节里,我会讲到它的具体用法。
# 还可以用`*`运算符把一个可迭代对象拆开作为函数的参数
divmod(20, 8)
(2, 4)
t = (20, 8)
divmod(*t)
(2, 4)
quotient, remainder = divmod(*t)
quotient, remainder
(2, 4)
# 让一个函数可以用元组的形式返回多个值
import os
_, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
filename
'idrsa.pub'
如果做的是国际化软件,那么_可能就不是一个理想的占位符,因为它也是 gettext.gettext 函数的常用别名,gettext 模块的文档里提到了这一点。在其他情况下,_会是一个很好的占位符
# 在元组拆包中使用*也可以帮助我们把注意力集中在元组的部分元素上
a, b, *rest = range(5)
a, b, rest
(0, 1, [2, 3, 4])
# 嵌套元组拆包
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)),
('Sao Paulo', 'BR', 19.649, (-23.547778,-46.635833)),
]
print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}' # :15 代表占位符,.4f 代表保留 4 位小数
for name, cc, pop, (latitude, longitude) in metro_areas: # ➋
if longitude <= 0: # ➌
print(fmt.format(name, latitude, longitude))
| lat. | long.
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
Sao Paulo | -23.5478 | -46.6358
具名元组
元组已经设计得很好用了,但作为记录来用的话,还是少了一个功能:我们时常会需要给记录中的字段命名。namedtuple 函数的出现帮我们解决了这个问题。
from collections import namedtuple
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35, 139))
tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35, 139))
tokyo.population # 你可以通过字段名或者位置来获取一个字段的信息
36.933
tokyo.coordinates
(35, 139)
tokyo[1]
'JP'
具名元组的属性和方法
# _fields 属性是一个包含这个类所有字段名称的元组
City._fields
('name', 'country', 'population', 'coordinates')
# 用_make( )通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟 City(*delhi_data)是一样的。
City._make(('Tokyo', 'JP', 36.933, (35, 139)))
City(name='Tokyo', country='JP', population=36.933, coordinates=(35, 139))
# _asdict( )把具名元组以 collections.OrderedDict 的形式返回,我们可以利用它来把元组里的信息友好地呈现出来。
City._asdict(tokyo)
{'name': 'Tokyo',
'country': 'JP',
'population': 36.933,
'coordinates': (35, 139)}
除了跟增减元素相关的方法之外,元组支持列表的其他所有方法,并且元组没有__reversed__
方法。
切片
为什么切片和区间会忽略最后一个元素:
- 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:
range(3)
和my_list[:3]
都返回 3 个元素。 - 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop-start)即可。
- 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成
my_list[:x]
和my_list[x:]
就可以了
# 我们还可以用 s[a:b:c]的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值。
s = 'bicycle'
s[::3]
'bye'
s[::-1]
'elcycib'
s[::-2]
'eccb'
使用切片对象,给切片命名:
invoice = """
0.....6................................40........52...55........
1909 Pimoroni PiBrella $17.50 3 $52.50
1489 6mm Tactile Switch x20 $4.95 2 $9.90
1510 Panavise Jr.-PV-201 $28.00 1 $28.00
1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
"""
SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split('\n')[2:]
for item in line_items:
print(item[UNIT_PRICE], item[DESCRIPTION])
$17.50 Pimoroni PiBrella
$4.95 6mm Tactile Switch x20
$28.00 1 Panavise Jr.-PV-201
$34.95 PiTFT Mini Kit 320x240
如果你有一个多维数组,你可以使用...来表示省略的维度
import numpy as np
# 创建一个三维数组
arr = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
# 使用...来访问第一维度的所有元素
print(arr[..., 0])
[[ 1 4]
[ 7 10]]
这段代码会打印出三维数组的第一维所有元素,即[[1, 4], [7, 10]]
。
给切片赋值:
如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作
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, 6, 7, 8, 9]
del l[5:7]
l
[0, 1, 20, 30, 6, 9]
l[3::2] = [11, 22]
l
[0, 1, 20, 11, 6, 22]
# 如果赋值的对象是一个切片,那么赋值语句的右侧必须是一个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。
l[2:5] = 100
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[38], line 2
1 # 如果赋值的对象是一个切片,那么赋值语句的右侧必须是一个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。
----> 2 l[2:5] = 100
TypeError: can only assign an iterable
对序列使用+
和*
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']]
# 第一个例子等同于
row = ['_'] * 3
board = []
for i in range(3):
row = ['_'] * 3
board.append(row)
# 第二个例子等同于
board = []
for i in range(3):
board.append(row)
序列的增量赋值
- 增量赋值运算符
+=
和*=
的表现取决于它们的第一个操作对象 +=
背后的特殊方法是__iadd__
- 但是如果一个类没有实现这个方法的话,Python 会退一步调用
__add__
- 上面所说的这些关于
+=
的概念也适用于*=
,不同的是,后者相对应的是__imul__
接下来有个小例子,展示的是*=在可变和不可变序列上的作用
l = [1, 2, 3]
id(l)
2933536739648
l *= 2
l
[1, 2, 3, 1, 2, 3]
id(l)
2933536739648
t = (1, 2, 3)
id(t)
2933536817280
t *= 2
id(t)
2933536707616
- 对于可变序列来说,执行增量乘法后,列表的 ID 没变,新元素追加到列表上。
- 对于不可变序列来说,执行增量乘法后,原来的元组被销毁,创建一个新的元组,然后把新元组的引用赋值给变量 t。
因此,对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
# 关于+=的谜题
t = (1, 2, [30, 40])
t[2] += [50, 60]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[51], line 3
1 # 关于+=的谜题
2 t = (1, 2, [30, 40])
----> 3 t[2] += [50, 60]
TypeError: 'tuple' object does not support item assignment
t
(1, 2, [30, 40, 50, 60])
没人料到的结果:t[2]
被改动了,但是也有异常抛出,过程如下:
- 将 s[a]的值存入 TOS(Top Of Stack,栈的顶端)。
- 计算 TOS+=b。这一步能够完成,是因为 TOS 指向的是一个可变对象。
- s[a]=TOS 赋值。这一步失败,是因为 s 是不可变的元组。
因此,你需要注意:
- 不要把可变对象放在元组里面
- 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
list.sort
方法和内置函数 sorted
- list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是 None 的原因。
- 与 list.sort 相反的是内置函数 sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。
- 不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数 reverse、key
- key 代表一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。
用返回 None 来表示就地改动这个惯例有个弊端,那就是调用者无法将其串联起来。而返回一个新对象的方法(比如说 str 里的所有方法)则正好相反,它们可以串联起来调用,从而形成连贯接口(fluent interface)
可选参数 key 还可以在内置函数
min( )
和max( )
中起作用。另外,还有些标准库里的函数也接受这个参数,像itertools.groupby( )
和heapq.nlargest( )
等
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']
用 bisect 来管理已排序的序列
bisect 模块包含两个主要函数,bisect 和 insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。
import bisect
import sys
HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}'
def demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle)
offset = position * ' |'
print(ROW_FMT.format(needle, position, offset))
if __name__ == '__main__':
if sys.argv[-1] == 'left':
bisect_fn = bisect.bisect_left
else:
bisect_fn = bisect.bisect
print('DEMO:', bisect_fn.__name__)
print('haystack->', ' '.join('%2d' % n for n in HAYSTACK))
demo(bisect_fn)
DEMO: bisect_right
haystack-> 1 4 5 6 8 12 15 20 21 23 23 26 29 30
31 @ 14 | | | | | | | | | | | | | |31
30 @ 14 | | | | | | | | | | | | | |30
29 @ 13 | | | | | | | | | | | | |29
23 @ 11 | | | | | | | | | | |23
22 @ 9 | | | | | | | | |22
10 @ 5 | | | | |10
8 @ 5 | | | | |8
5 @ 3 | | |5
2 @ 1 |2
1 @ 1 |1
0 @ 0 0
bisect 函数其实是 bisect_right 函数的别名,后者还有个姊妹函数叫 bisect_left。它们的区别在于,bisect_left 返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面,而 bisect_right 返回的则是跟它相等的元素之后的位置
用bisect.insort
插入新元素
排序很耗时,因此在得到一个有序序列之后,我们最好能够保持它的有序。bisect.insort 就是为了这个而存在的。
import bisect
import random
SIZE = 7
random.seed(1729)
my_list = []
for i in range(SIZE):
new_item = random.randrange(SIZE*2)
bisect.insort(my_list, new_item)
print('%2d->' % new_item, my_list)
10-> [10]
0-> [0, 10]
6-> [0, 6, 10]
8-> [0, 6, 8, 10]
7-> [0, 6, 7, 8, 10]
2-> [0, 2, 6, 7, 8, 10]
10-> [0, 2, 6, 7, 8, 10, 10]
当列表不是首选时
虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。
- 比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。
- 再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。
数组:
如果我们需要一个只包含数字的列表,那么array.array
比list
更高效。数组支持所有跟可变序列有关的操作,包括.pop、.insert 和.extend
。另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes 和.tofile
。
Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char),因此array('b')
创建出的数组就只能存放一个字节大小的整数,范围从-128 到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存放除指定类型之外的数据
# 一个浮点数数组的创建、存入文件和从文件读取的过程
from array import array
from random import random
floats = array('d', (random() for i in range(10**7)))
floats[-1]
0.5963321947530882
fp = open('floats.bin', 'wb')
floats.tofile(fp) # 把数组存入一个二进制文件里
fp.close()
floats2 = array('d')
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7) # 从二进制文件里读取 1000 万个浮点数到数组里
fp.close()
floats2[-1]
0.5963321947530882
floats2 == floats # 两个数组相等
True
array.tofile
和array.fromfile
用起来很简单。把这段代码跑一跑,你还会发现它的速度也很快。
另外一个快速序列化数字类型的方法是使用 pickle 模块。
pickle.dump
处理浮点数组的速度几乎跟array.tofile
一样快。不过前者可以处理几乎所有的内置数字类型,包含复数、嵌套集合,甚至用户自定义的类。前提是这些类没有什么特别复杂的实现。
从 Python 3.4 开始,数组类型不再支持诸如list.sort( )
这种就地排序方法。要给数组排序的话,得用 sorted 函数新建一个数组
内存视图:
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片
内存视图其实是泛化和去数学化的 NumPy 数组。它让你在不需要复制内容的前提下,在数据结构之间共享内存。其中数据结构可以是任何形式,比如 PIL 图片、SQLite 数据库和 NumPy 的数组,等等。这个功能在处理大型数据集合的时候非常重要。
memoryview.cast
的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast
会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。
# 通过改变数组中的一个字节来更新数组里某个元素的值
numbers = array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
len(memv)
5
memv[0]
-2
memv_oct = memv.cast('B') # 把 memv 里的内容转换成'B'类型,也就是无符号字符
memv_oct.tolist()
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
memv_oct[5] = 4 # 把位于位置 5 的字节赋值成 4
numbers
array('h', [-2, -1, 1024, 1, 2])
Numpy 和 SciPy
- NumPy 实现了多维同质数组(homogeneous array)和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。通过 NumPy,用户能对这些数据结构里的元素进行高效的操作。
- SciPy 是基于 NumPy 的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。SciPy 的高效和可靠性归功于其背后的 C 和 Fortran 代码,而这些跟计算有关的部分都源自于 Netlib 库。换句话说,SciPy 把基于 C 和 Fortran 的工业级数学计算功能用交互式且高度抽象的 Python 包装起来,让科学家如鱼得水。
# 对 numpy.ndarray 的行和列进行基本操作
a = np.arange(12)
a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
type(a)
numpy.ndarray
a.shape
(12,)
a.shape = 3, 4
a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
a[2]
array([ 8, 9, 10, 11])
a[2, 1]
9
a[:, 1] # 第二列
array([1, 5, 9])
a.transpose() # 转置
array([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])
双向队列和其他形式的队列
利用.append
和.pop
方法,我们可以把列表当作栈或者队列来用(比如,把.append
和.pop(0)
合起来用,就能模拟队列的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
collections.deque
类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。
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) # 右移 3 位
dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
dq.rotate(-4) # 左移 4 位
dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
dq.appendleft(-1) # 左边加入-1
dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
dq.extend([11, 22, 33]) # 右边加入 11, 22, 33
dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
dq.extendleft([10, 20, 30, 40]) # 左边加入 10, 20, 30, 40
dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
其他队列:
- queue
- multiprocessing
- asyncio
- heapq