序列
Python 使用统一的风格去处理序列数据,不管是哪种数据结构,它们都共用一套丰富的操作:迭代,切片,排序和拼接。通过了解 Python 不同的序列类型以及它们的使用,能够让我们避免重复发明轮子,而且 Python 的 API 能够帮助我们把自己定义的结构设计得跟原生序列一样。
序列类型
-
根据是否能放不同类型的数据分类
- 容器序列:能存放不同类型的数据,存放的是对象的引用。其中包括:
list
,tuple
,collections.deque
等。 - 扁平序列:仅能存放基础类型的对象,如字符,字节,数值。其中包括:
str
,bytes
,bytearray
,memoryview
,array.array
。
- 容器序列:能存放不同类型的数据,存放的是对象的引用。其中包括:
-
根据容器内数据是否可变分类
- 可变序列:
list
,bytearray
,array.array
,collections.deque
和memoryview
- 不可变序列:
tuple
,str
和bytes
- 可变序列:
列表推导和生成器表达式
列表推导
一个例子
symbols = 'abcdefg'
codes = [ord(symbol) for symbol in symbols]
- 优势:简单,可读。
- 劣势:在使用列表推导时,会建立一个完整的列表,然后再将列表传递出去,某些情况下会有额外的内存占用。
- 如果列表推导的代码超过两行,则需要考虑是不是得用for循环重写了。
变量泄露的问题
在 python2 中,列表推导中的变量会覆盖外部的同名变量,而在 python3 中则不会。
Python 2.7.17 (default, Apr 15 2020, 17:20:14)
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'abcd efg'
>>> dummy = [x for x in 'pouyr']
>>> x
'r'
Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'abcd efg'
>>> dummy = [x for x in 'pouyr']
>>> x
'abcd efg'
生成器表达式
一个例子
Python 2.7.17 (default, Apr 15 2020, 17:20:14)
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> x = 'abcd efg'
>>> tuple(ord(a) for a in x)
(97, 98, 99, 100, 32, 101, 102, 103)
>>> import array
>>> array.array('I', (ord(a) for a in x))
array('I', [97L, 98L, 99L, 100L, 32L, 101L, 102L, 103L])
- 如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来,否则括号是必须的。
- 生成器表达式逐个产出元素。
元组
- 元组是不可变的列表
- 元组可用于记录(没啥用)
元组拆包
一个例子
>>> a= (1 , 2)
>>> x, y = a
>>> x
1
>>> y
2
- 用
*
来处理剩下的元素, 在 python 中,函数用*args
来获取不确定数量的参数算是一种经典的写法了。
切片
一个例子
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
a:b:c
这种用法只能作为索引或者下标在[]
中来返回一个切片对象: slice(a, b, c)
, 当使用 seq[start:stop:step]
时,python实际会调用seq.__getitem__(slice(start, stop, step))
。虽然我们还不了解这个切片对象,但知道这个原理足以让我们做一些骚操作了。比如解析一些纯文本文件,使用有名字的切片比硬编码的数字区间要方便得多。
对序列使用 +
和 *
>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
-
注意:在
a * n
这个语句中,如果a
里的元素是对其他可变对象的引用的话,那么实际上会产生 n 个对该对象的引用。 -
Good Case
>>> bord = [['_'] * 3 for i in range(3)]
>>> bord
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> bord[1][2] = 'X'
>>> bord
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
- Bad Case
>>> bord2 = [['_'] * 3] * 3 # 外面的这个*3会导致重复生成3个['_']的引用
>>> bord2
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> bord2[1][2] = 'X'
>>> bord2
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]
序列的增量赋值
+=
背后的特殊方法是__iadd__
,如果没有实现这个方法,Python会退一步使用__add__
。考虑下面的这个简单的表达式:
>>> a += b
- 如果
a
实现了__iadd__
方法,对可变序列来说,a
就会就地改动,就像调用了a.extend(b)
一样。但如果a
没有实现__iadd__
的话,a += b
这个表达式的效果就变成了a = a + b
,首先计算a + b
,得到一个新的对象,然后赋值给a
。也就是说,在这个表达式中,变量名会不会关联到新的对象,完全取决于这个类型有没有实现__iadd__
这个方法。
>>> l = [1, 2, 3]
>>> id(l)
140306575506720
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
140306575506720 # 结果不变
>>>
>>> t = (1, 2, 3)
>>> id(t)
140306575504240
>>> t *= 2
>>> id(t)
140306575887840 # 结果会变
一个问题
>>> 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])
我们看看这个表达式背后的字节码(虽然看不懂)
>>> dis.dis('s[a]+=b')1 0 LOAD_NAME 0 (s)2 LOAD_NAME 1 (a)4 DUP_TOP_TWO6 BINARY_SUBSCR8 LOAD_NAME 2 (b)10 INPLACE_ADD12 ROT_THREE14 STORE_SUBSCR16 LOAD_CONST 0 (None)18 RETURN_VALUE
上述操作会在第14步失败,因为s是不可变的元组。这其实是个非常罕见的边界情况,在2个月的Python生涯中,我还没见过谁在这个地方吃过亏。
- 不要把可变对象放在元组里面。
- 增量赋值不是一个原子操作。虽然它抛出了异常,但还是完成了操作。
- 多看看Python的字节码,它对我们了解代码的背后的运行机制很有帮助。
list.sort
方法和内置函数sorted
list.sort
就地排序,返回值是None
。而内置函数sorted
会新建一个列表作为返回值。- 两个关键字
reverse
True
为降序输出,False
为升序输出,默认值为False
。key
- 一个只有一个参数的函数,产生的结果将是排序算法依赖的对比关键字。
用bisect
来管理已排序的序列
>>> import bisect
>>> a = [1, 1, 2, 2, 3, 4, 5, 6]
>>> bisect.bisect_left(a, 3)
4
>>> bisect.bisect(a, 2)
4
bisect
类似 C++ 的upper_bound
函数,找到第一个大于该值的下标,而bisect_left
类似 C++ 的lower_bound
函数,找到第一个大于等于该值的下标。- 复杂度: O(log(n)) (二分找)
用 bisect.insort
插入新元素
>>> import bisect
>>> a = [1, 1, 2, 2, 3, 4, 5, 6]
>>> bisect.insort(a, 4)
[1, 1, 2, 2, 3, 4, 4, 5, 6]
- 同理它有个变体叫
insort_left
, 这个变体背后用的是bisect_left
. - 复杂度: O(log(n))(二分找) + O(n)(往里塞)
其他序列
数组
- 如果我们需要一个只包含数字的列表,那么
array.array
比list
更高效。数组支持所有跟可变序列有关的操作。另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes
和.tofile
。 - Python 数组跟 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.36472462317072263
>>> 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.36472462317072263
>>> floats2 == floats
True
内存视图
memoryview
是一个内置类,它能让用户不复制内容的情况下操作一个数组的不同切片。
memoryview.cast
的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。memoryview.cast
会把同一块内存里的内容打包成一个全新的memoryview
对象给你。
>>> from array import array
>>> numbers = array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)
>>> len(memv)
5
>>> memv[0]
-2
>>> memv_oct = memv.cast('B')
>>> memv_oct.tolist()
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4
>>> numbers
array('h', [-2, -1, 1024, 1, 2])
- 位置5是下标为2的元素的高位,高位变成了4,整体的值则变成了1024
双向队列和其他形式的队列
collections.deque
类(双向队列)是一个线程安全的队列,可以指定队列的大小,如果队列满了,则会删除头部的元素。append
和popleft
都是原子操作,也就是说deque
可以在多线程中安全的使用,而使用者不需要担心资源锁的问题。
其他队列
queue包
queue
包提供了同步(线程安全)类Queue
, LifoQueue
和PriorityQueue
,不同的线程可以利用这些数据类型来交换信息。这三个类的构造方法都有一个可选参数maxsize
, 它接受正整数作为输入值,用来限定队列的大小。但是在满员的时候,这些类不会扔掉旧的元素来腾出位置。相反,如果队列满了,它会被锁住,直到另外的线程移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量。
multiprocessing
包
这个包实现了自己的Queue
, 它跟queue.Queue
类似,是设计给进程通信用的。同时还有一个专门的multiprocessing.JoinableQueue
类型,可以让任务管理变得更方便。
asyncio
包
Python 3.4 新提供的包,里面有Queue
, LifoQueue
, PriorityQueue
和 JoinableQueue
这些类受到 queue
和 multiprocessing
模块的影响,但是为异步编程里的任务管理提供了专门的便利。
heapq
跟上面三个模块不同的是,heapq
没有队列类,而是提供了heappush
和 heappop
方法,让用户可以把可变序列当做堆队列或者优先队列来使用。
第二章的内容真多,写了一晚上。。主要内容大概就是Python的几个序列。接下来的内容就是字典和集合啦!