第2章 序列构成的数组
2.1 序列类型概述
Python标准库用C实现了丰富的序列类型:
-
容器序列
list
、tuple
、collections.deque
这些序列能存放不同类型的数据 -
扁平序列
str
、bytes
、bytearray
、memoryview
、array.array
这些序列只能容纳一种类型
容器序列存放的是对象的引用,而扁平序列里存放的是对象的值而不是引用。即,扁平序列其实是一段连续的内存空间,但它里面只能存放诸如字符、字节和数值这种基本类型。
从序列类型是否能被修改,可以分为:
-
可变序列(Mutable Sequence)
list
、bytearray
、array.array
、collections.deque
、memoryview
-
不可变序列(Sequence)
tuple
、str
、bytes
2.2 列表推导
下面介绍list
,开始于列表推导(list comprehension)
>>> x = 'ABC'
>>> dummy = [ord(x) for x in x]
注意:在python2.x中,列表推导里for
关键词后的赋值操作可能会影响列表推导上下文中的同名变量,但这一问题在Python3中不会造成影响。
句法提示:Python会忽略代码里[]
、{}
、()
中的换行。(Python中的续行符:\
)
列表推导同filter
和map
的比较
>>> symbols = '$¢£¥€¤'
>>> beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
>>> beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
可见,虽然filter
和map
结合起来也能达到列表推导的效果,但明显更复杂,而且其效率也不一定比列表推导快。
总之,列表推导的作用只有一个:生成列表,如果想生成其他序列类型,就要用到生成器表达式。
2.3 生成器表达式
要生成一个tuple
、array.array
等序列类型,显然我们可以先用列表推导生成一个list
,然后再将list
转成其他序列类型。但生成器表达式是更好的选择。这是因为生成器表达式可以逐个的产出元素,而不是先建立一个完整的列表,然后再将其传入某个构造函数里。显然生成器表达式更能节省内存。
生成器表达式的语法跟列表推导差不多,只不过将方括号换成了圆括号。
>>> symbols = '$¢£¥€¤'
>>> tuple(ord(x) for x in symbols)
>>> import array
>>> array.array('I', (ord(x) for x in symbols)) # 第一个参数I表示数组中存储值的类型是long
注: 当生成器表达式是函数调用的唯一参数时,可以省略括号。
2.4 元组
关于元组,首先要明确的一点是:元组不仅仅是不可变的列表。
元组可用于对数据的记录
元组中每个元素都有其对应的位置,正是这一点能让我们可以将元组当成记录来用,即赋予元组每个位置一个具体的含义。
>>> lax_coordinates = (33.9425, -118.4080)
>>> city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
>>> traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDZ208556')]
>>> for country, _ in traveler_ids:
... print(country)
注: _
是占位符。
元组拆包
在上例中,我们把('Tokyo', 2003, 32450, 0.66, 8014)
里的元素分别赋给了变量city
, year
, pop
, chg
, area
, 只用一行代码就完成了所有的赋值工作,让我们再来看元组拆包的例子:
>>> lax_coordinates = (33.9425, -118.408056)
>>> lattitude, longitude = lax_coordinates
在Python中可以这样交换两个变量的值:
>>> b, a = a, b
上述两个都是平行赋值的例子,所谓平行赋值,就是把一个可迭代对象里的元素,一并赋值到对应的变量组成的元组中去。
此外,还可以用*
将一个可迭代对象拆开作为函数的参数:
>>> divmod(20, 8) # divmod(a, b) 函数把除数和余数运算结果结合起来,返回一个包含商和余数的元组(a // b, a % b)。
(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 # rest为什么是一个list呢?
(0, 1, [2, 3, 4])
>>> 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)
嵌套元组拆包
接受表达式的元组本身可以是嵌套的,如(a, b, (c, d))
。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python就可以作出正确的对应。
>>> metro_area = ('Tokyo', 'Japan', 36.933, (35.689, 139.691))
>>> name, cc, pop, (lat, lon) = metro_area
('Tokyo', 'Japan', 36.933, (35.689, 139.691))
具名元组
前面提到,元组可用于对数据的记录,每一个位置都可以有表达的含义。在Python中,我们可以对记录中的字段命名,这需要用到collections.namedtuple
, 这是一个工厂函数。它可以用来创建一个带字段名的元组和一个有名字的类:
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates')
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689, 139.6916))
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689, 139.6916))
>>> tokyo.country
'JP'
>>> tokyo[3]
(35.689, 139.6916)
-
创建一个具名元组需要两个参数:类名和类各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。
-
可以通过字段名或者位置来获取对应的元素
具名元组具有一些自己专属的属性:_fields
类属性、类方法_make(iterable)
、和实例方法_asdict()
。
>>> City._fields
('name', 'country', 'population', 'coordinates')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
>>> delhi = City._make(delhi_data)
>>> delhi._asdict()
{'name': 'Delhi NCR',
'country': 'IN',
'population': 21.935,
'coordinates': (28.613889, 77.208889)}
_fields
类属性是一个包括这个类所有字段名称的元组- 用
_make()
通过接受一个可迭代对象来生成类的一个实例,作用相当于City(*delhi_data)
_asdict()
将具名元组的信息以dict的形式返回
元组和列表的相似度
除了不支持增减元素相关的方法外,元组支持列表的其他所有方法。
2.5 切片
在python中,列表、元组、字符串这类序列类型都支持切片操作。
切片和区间操作会忽略最后一个元素:这符合C语言以0为起始下标的传统,即左闭右开区间。
这样做其实是有几点好处的:
- 当只有最有一个位置信息时,很容易看出来有几个元素。例如
range(3)
和mylist[:3]
都有3个元素。- 当起始位置都知道时,可以用 end-start 快速计算区间长度。
- 可以用任一下标将序列分成不重叠的两部分,只要写成
mylist[:x]
和mylist[x:]
就可以了。
具体的切片操作
可以用s[a:b:c]
的形式从s中在a和b之间以c为间隔取值。同时c的值可以为负,负意味着反向取值。
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
a:b:c
这种用法只能作为索引或者下标用在[]
中返回一个切片对象:slice(a, b, c)
。当然我们也可以手动地对切片命名,例如:
>>> SKU = slice(0, 2)
>>> s[SKU]
'bi'
多维切片和省略(…)
给切片赋值
如果切片放在赋值语句的左边,或者把它作为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, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
注 :如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象,即使是单独的一个值,也需要将其转换成可迭代对象。
2.6 对序列使用+
和*
>>> 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
里的元素是其他可变对象的引用时,结果可能会出乎意料。
2.7 序列的增量赋值
增量赋值运算符+=
和*=
的表现取决于他们的第一个操作对象。
+=
背后是特殊方法 __iadd__
(就地加法)。但是如果一个类没有实现 __iadd__
, 则Python会退一步调用 __add__
。
>>> a += b
对于上面这个表达式,如果a
是可变对象,那么会就地改动。但如果a
没有实现__iadd__
, 则这个表达式的效果变得和a = a + b
一样了:首先计算a+b
,得到一个新的对象,再赋值给a
。一般来说,可变序列都实现了 __iadd__
方法,对于不可变序列来说,根本就不支持这个操作。
关于增量赋值有一个谜题值得探讨:
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<ipython-input-53-d877fb0e9d36>", line 1, in <module>
t[2] += [50, 60]
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
- 不要把可变对象放在元组里
- 增量赋值不是一个原子操作
- 可使用
dis.dis(sentence)
查看语句后的字节码
2.8 list.sort
方法和内置函数sorted
list.sort
会就地排序列表,返回值为None
sorted
可接受任一可迭代对象作为参数,同时新建一个列表作为返回值。
这两个函数都有两个可选的关键字参数 reverse
和 key
。
2.9 用bisect来管理已排序的序列
bisect
模块主要包含两个主要函数,bisect
和insort
, 两个函数都利用二分查找算法在有序序列中查找或者插入元素。
2.10 列表并非是首选
数组
当我们需要一个只包含数字的列表时,array.array
会比 list
更高效。 因为数组背后存的不是Python中的float
对象,而是字节表示。
内存视图
memoryview 是一个内置类,可以让用户在不复制内容的情况下操作同一个数组的不同切片。个人感觉是内存中的内容不变,改变读取它的方式(如以无符号整型读取或者以有符号整型读取),读取的结果完全不同。
NumPy和SciPy
无需多言。
双向队列和其他形式的队列
collections.deque
、 queue
、 multiprocessing
等
本文只是对书中的主要内容做了小结,对于不常用到的内容只是稍微提及,以后需要用到的时候再查。若想更详细地了解,还是强烈推荐看原书,书中的示例代码尽显Pythonic
,读起来酣畅淋漓,醍醐灌顶。