序列类型
Python 标准库用 C 实现了丰富的序列类型, 列举如下。
容器序列
list、 tuple 和 collections.deque 这些序列能存放不同类型的数据。
扁平序列
str、 bytes、 bytearray、 memoryview 和 array.array, 这类序列只能容纳一种类型。
容器序列存放的是它们所包含的任意类型的对象的引用, 而扁平序列
里存放的是值而不是引用。 换句话说, 扁平序列其实是一段连续的内存空间。 由此可见扁平序列其实更加紧凑, 但是它里面只能存放诸如字符、 字节和数值这种基础类型。序列类型还能按照能否被修改来分类。
可变序列
list、 bytearray、 array.array、 collections.deque 和
memoryview。
不可变序列
tuple、 str 和 bytes。
图 2-1 显示了可变序列( MutableSequence) 和不可变序列
( Sequence) 的差异, 同时也能看出前者从后者那里继承了一些方
法。 虽然内置的序列类型并不是直接从 Sequence 和
MutableSequence 这两个抽象基类( Abstract Base Class, ABC) 继承而来的, 但是了解这些基类可以帮助我们总结出那些完整的序列类型包含了哪些功能。
2.2 列表推导和生成器表达式
说白了主要就是 for 循环
2.2.3 笛卡儿积
>>> 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')]
2.2.4 生成器表达式
生成器表达式的语法跟列表推导差不多, 只不过把方括号换成圆括号而
已
>>> 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])
2.3 元组不仅仅是不可变的列表
有些 Python 入门教程把元组称为“不可变列表”, 然而这并没有完全概括
元组的特点。 除了用作不可变的列表, 它还可以用于没有字段名的记
录。 鉴于后者常常被忽略, 我们先来看看元组作为记录的功用
2.3.1 元组和记录
元组其实是对数据的记录: 元组中的每个元素都存放了记录中一个字段
的数据, 外加这个字段的位置。 正是这个位置信息给数据赋予了意义。
In [210]: lax_coordinates = (33.9425, -118.408056)
In [211]: city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
In [212]: traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'),('ESP', 'XDA2
...: 05856')]
In [213]: for passport in sorted(traveler_ids):
...: print('%s/%s' % passport)
...:
BRA/CE342567
ESP/XDA205856
USA/31195855
In [214]: for country, _ in traveler_ids: # 说白了就是花式运用拆包特性
...: print(country) # 上方 _ 为占位符
...:
USA
BRA
ESP
2.3.2 元组拆包
最好辨认的元组拆包形式就是平行赋值, 也就是说把一个可迭代对象里的元素, 一并赋值到由对应的变量组成的元组中。
>>> lax_coordinates = (33.9425, -118.408056)
>>> latitude, longitude = lax_coordinates # ??元组拆包
>>> latitude
33.9425
>>> longitude
-118.408056
另外一个很优雅的写法当属不使用中间变量交换两个变量的值:
>>> b, a = a, b
在 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, [])
2.3.3 嵌套元组拆包
In [221]: a,b,c = (1,2,(3,4))
In [222]: a
Out[222]: 1
In [223]: c
Out[223]: (3, 4)
2.3.4 具名元组
collections.namedtuple 是一个工厂函数, 它可以用来构建一个带
字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大
帮助。
用 namedtuple 构建的类的实例所消耗的内存跟元组是一样的, 因为字段名都被存在对应的类里面。 这个实例跟普通的对象实例比起来也要小一些, 因为 Python 不会用 __dict__
来存放这些实例的属性。
Card = collections.namedtuple('Card', ['rank', 'suit']) # 构建范例
如何用具名元组来记录一个城市的信息:
>>> 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
? 创建一个具名元组需要两个参数, 一个是类名, 另一个是类的各个
字段的名字。 后者可以是由数个字符串组成的可迭代对象, 或者是由空格分隔开的字段名组成的字符串。
? 存放在对应字段里的数据要以一串参数的形式传入到构造函数中
( 注意, 元组的构造函数却只接受单一的可迭代对象)
? 你可以通过字段名或者位置来获取一个字段的信息。
建立由列表组成的列表
有时我们会需要初始化一个嵌套着几个列表的列表, 譬如一个列表可能需要用来存放不同的学生名单, 或者是一个井字游戏板 上的一行方
块。 想要达成这些目的, 最好的选择是使用列表推导。
一个包含 3 个列表的列表, 嵌套的 3 个列表各自有 3 个元素来代表井字游戏的一行方块 :
>>> board = [['_'] * 3 for i in range(3)] ?
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' ?
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
? 建立一个包含 3 个列表的列表, 被包含的 3 个列表各自有 3 个元
素。 打印出这个嵌套列表。
? 把第 1 行第 2 列的元素标记为 X, 再打印出这个列表。
2.6 序列的增量赋值
+=
背后的特殊方法是 __iadd__
( 用于“就地加法”) 。 但是如果一个类
没有实现这个方法的话, Python 会退一步调用 __add__
。 考虑下面这
个简单的表达式:
>>> a += b
如果 a 实现了 __iadd__
方法, 就会调用这个方法。 同时对可变序列
( 例如 list、 bytearray 和 array.array) 来说, a 会就地改动, 就
像调用了 a.extend(b)
一样。 但是如果 a 没有实现 __iadd__
的话, a += b
这个表达式的效果就变得跟 a = a + b
一样了: 首先计算 a + b
, 得到一个新的对象, 然后赋值给 a。 也就是说, 在这个表达式中,变量名会不会被关联到新的对象, 完全取决于这个类型有没有实现
__iadd__
这个方法。
总体来讲, 可变序列一般都实现了__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 ? 运用增量乘法后, 新的元组被创建
??一个关于+=的谜题 (有趣)
In [257]: t = (1,2,[30,40])
In [258]: t[2]
Out[258]: [30, 40]
In [259]: type(t)
Out[259]: tuple
In [260]: type(t[2])
Out[260]: list
In [262]: t[2] += [50,60]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-262-b483a1b4fe1d> in <module>()
----> 1 t[2] += [50,60]
TypeError: 'tuple' object does not support item assignment
结果:
t 变成 (1, 2, [30, 40, 50, 60])
因为 tuple 不支持对它的元素赋值, 所以会抛出 TypeError 异常
教训:不要把可变对象放在元组里面
2.7 list.sort方法和内置函数sorted
list.sort 方法会就地排序列表, 也就是说不会把原列表复制一份。 这
也是这个方法的返回值是 None 的原因, 提醒你本方法不会新建一个列
表。 在这种情况下返回 None 其实是 Python 的一个惯例.
与 list.sort 相反的是内置函数 sorted, 它会新建一个列表作为返回值。 这个方法可以接受任何形式的可迭代对象作为参数, 甚至包括不可变序列或生成器
2.10 本章小结
要想写出准确、 高效和地道的 Python 代码, 对标准库里的序列类型的掌握是不可或缺的。
Python 序列类型最常见的分类就是可变和不可变序列。 但另外一种分类方式也很有用, 那就是把它们分为扁平序列和容器序列。 前者的体积更小、 速度更快而且用起来更简单, 但是它只能保存一些原子性的数据,比如数字、 字符和字节。 容器序列则比较灵活, 但是当容器序列遇到可变对象时, 用户就需要格外小心了, 因为这种组合时?;岣愠鲆恍耙馔狻?, 特别是带嵌套的数据结构出现时, 用户要多费一些心思来保证代码的正确。
列表推导和生成器表达式则提供了灵活构建和初始化序列的方式, 这两个工具都异常强大。 如果你还不能熟练地使用它们, 可以专门花时间练习一下。 它们其实不难, 而且用起来让人上瘾元组在 Python 里扮演了两个角色, 它既可以用作无名称的字段的记录,又可以看作不可变的列表。 当元组被当作记录来用的时候, 拆包是最安全可靠地从元组里提取不同字段信息的方式。 新引入的*
句法让元组拆包的便利性更上一层楼, 让用户可以选择性忽略不需要的字段。 具名元组也已经不是一个新概念了, 但它似乎没有受到应有的重视。 就像普通元组一样, 具名元组的实例也很节省空间, 但它同时提供了方便地通过名字来获取元组各个字段信息的方式, 另外还有个实用的 ._asdict()
方法来把记录变成 OrderedDict 类型。
Python 里最受欢迎的一个语言特性就是序列切片, 而且很多人其实还没完全了解它的强大之处。 比如, 用户自定义的序列类型也可以选择支持NumPy 中的多维切片和省略( ...) 。 另外, 对切片赋值是一个修改可变序列的捷径。
重复拼接 seq * n 在正确使用的前提下, 能让我们方便地初始化含有
不可变元素的多维列表。 增量赋值 += 和 *= 会区别对待可变和不可变序列。 在遇到不可变序列时, 这两个操作会在背后生成新的序列。 但如果被赋值的对象是可变的, 那么这个序列会就地修改——然而这也取决于序列本身对特殊方法的实现。
序列的 sort
方法和内置的 sorted
函数虽然很灵活, 但是用起来都不
难。 这两个方法都比较灵活, 是因为它们都接受一个函数作为可选参数来指定排序算法如何比较大小, 这个参数就是 key 参数。 key 还可以被用在 min 和 max 函数里。 如果在插入新元素的同时还想保持有序序列的顺序, 那么需要用到bisect.insort
。 bisect.bisect
的作用则是快速查找。
除了列表和元组, Python 标准库里还有 array.array。 另外, 虽然NumPy 和 SciPy 都不是 Python 标准库的一部分, 但稍微学习一下它们,会让你在处理大规模数值型数据时如有神助。