Python 用统一的风格去处理序列数据。不管是哪种数据结构,字符串、列表、字节序列、数组、XML 元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。
Python 标准库用 C 实现了丰富的序列类型,列举如下。
容器序列
list、tuple 和 collections.deque 这些序列能存放不同类型的数据。
扁平序列
str、bytes、bytearray、memoryview 和 array.array,这类序列只能容纳一种类型。
容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是
引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧
凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。
序列类型还能按照能否被修改来分类。
下图显示了可变序列(MutableSequence)和不可变序列(Sequence)的差异,同时也
能看出前者从后者那里继承了一些方法。
列表推导可以帮助我们把一个序列或是其他可迭代类型中的元素过滤或是加工,然后再新建一个列表。
列表推导式:
l = [function(i) for i in iterable]
生成器推导式:
t = (function(i) for i in iterable)
# 用生成器表达式初始化元组和数组
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(symbol) for symbol in symbols)
(36, 162, 163, 165, 8364, 164)
>>> import array
>>> array.array('I', (ord(symbol) for symbol in symbols))
array('I', [36, 162, 163, 165, 8364, 164])
列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在 Python 3 中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。
元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个
字段的位置。正是这个位置信息给数据赋予了意义。
如果只把元组理解为不可变的列表,那其他信息——它所含有的元素的总数和它们的位
置——似乎就变得可有可无。但是如果把元组当作一些字段的集合,那么数量和位置信息
就变得非常重要了。
两种常见形式:
a,b,_ = (1,2,'years')
b, a = a, b
运算符*把一个可迭代对象拆开作为函数的参数:
>>> divmod(20, 8)
(2, 4)
>>> t = (20, 8)
>>> divmod(*t)
(2, 4)
>>> quotient, remainder = divmod(*t)
>>> quotient, remainder
(2, 4)
用*来处理剩下的元素,在 Python 中,函数用 *args
来获取不确定数量的参数算是一种经典写法了:
>>> 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, [])
在平行赋值中,* 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置
collections.namedtuple
是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
用namedtuple
构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为Python 不会用__dict__
来存放这些实例的属性。
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'
>>> City._fields
('name', 'country', 'population', 'coordinates')
>>> LatLong = namedtuple('LatLong', 'lat long')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
>>> delhi = City._make(delhi_data)
>>> delhi._asdict()
OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population',
21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))])
>>> for key, value in delhi._asdict().items():
print(key + ':', value)
name: Delhi NCR
country: IN
population: 21.935
coordinates: LatLong(lat=28.613889, long=77.208889)
除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性,几个最有用的:_fields 类属性、类方法_make(iterable)
和实例方法 _asdict()
[start:stop:step]
对 seq[start:stop:step]
进 行 求 值 的 时 候,Python 会 调 用 seq.__getitem__(slice(start, stop, step))
。
在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。
my_ list[:x]
和 my_list[x:]
就可以了如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独
一个值,也要把它转换成可迭代的序列。
Python 程序员会默认序列是支持 + 和 * 操作的。通常 + 号两侧的序列由相同类型的数据所
构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会新建一个包含同样类
型数据的序列来作为拼接的结果。
+
和*
都遵循这个规律,不修改原有的操作对象,而是构建一个全新的序列。适用于完成简单的复制和拼接操作。
⭐注意:如果在 a * n 这个语句中,序列 a 里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用my_list = [[]] * 3
来初始化一个由列表组成的列表,但是你得到的列表里包含的 3 个元素其实是 3 个引用,而且这 3 个引用指向的都是同一个列表。
有时我们会需要初始化一个嵌套着几个列表的列表,最好的选择是使用列表推导。
# 列表推导式
>>> 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__
(用于“就地加法”)。但是如果一个类没有实现这个方法的
话,Python 会退一步调用 __add__
。
如 果 a
实 现 了 __iadd__
方 法, 就 会 调 用 这 个 方 法。 同 时 对 可 变 序 列( 例 如 list、
bytearray
和 array.array
)来说,a 会就地改动,就像调用了 a.extend(b)
一样。但是如
果 a 没有实现 __iadd__
的话,a += b
这个表达式的效果就变得跟a = a + b
一样了:首先
计算 a + b
,得到一个新的对象,然后赋值给a
。也就是说,在这个表达式中,变量名会不
会被关联到新的对象,完全取决于这个类型有没有实现 __iadd__
这个方法。
>>> 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]
到底会发生下面 4 种情况中的哪一种?
(1, 2, [30, 40, 50, 60])
。TypeError
异常。难以想象,答案是d!
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "" , 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)
3 LOAD_NAME 1(a)
6 DUP_TOP_TWO
7 BINARY_SUBSCR
8 LOAD_NAME 2(b)
11 INPLACE_ADD
12 ROT_THREE
13 STORE_SUBSCR
14 LOAD_CONST 0(None)
17 RETURN_VALUE
3 个教训:
• 不要把可变对象放在元组里面。
• 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
• 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。
Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None
,好让调用者知道传入的参数发生了变动,而且并未产生新的对象。
list.sort
方法会就地排序列表,也就是说不会把原列表复制一份,因此返回值为None。与 list.sort
相反的是内置函数sorted
,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器(见第 14 章)。而不管sorted
接受的是怎样的参数,它最后都会返回一个列表。
不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参数:
key=str
. lower 来实现忽略大小写的排序,或者是用 key=len
进行基于字符串长度的排序。bisect(haystack, needle) 在 haystack(干草垛)里搜索 needle(针)的位置,该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。也就是在说这个函数返回的位置前面的值,都小于或等于 needle 的值。其中 haystack 必须是一个有序的序列。你可以先用 bisect(haystack, needle)
查找位置 index,再用 haystack.insert(index, needle)
来插入新值。但你也可用insort
来一步到位,并且后者的速度更快一些。
# 在有序序列中用 bisect 查找某个元素的插入位置
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)
虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque
(双端队列)的速度应该会更快。
如果我们需要一个只包含数字的列表,那么array.array
比list
更高效。数组支持所有跟可变序列有关的操作,包括 .pop
、.insert
和 .extend
。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes
和.tofile
。
memoryview
是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview.cast
的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型转换的概念差不多。memoryview.cast
会把同一块内存里的内容打包成一个全新的memoryview
对象给你。
利用 .append
和.pop
方法,我们可以把列表当作栈或者队列来用(比如,把 .append
和.pop(0)
合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
collections.deque
类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这差不多。memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。