Python 从ABC 语言继承一种特点:用统一的风格去处理序列数据。不管是哪种数据结构,字符串、列表、字节序列、数组、XML 元素,抑或是数据库查询结果,它们都共用一套丰富的操作:迭代、切片、排序,还有拼接。
2.1 内置序列类型概览
序列类型(按容器与否):
- 容器序列
list
、tuple
和collections.deque
这些序列能存放不同类型的数据。 - 扁平序列
str
、bytes
、bytearray
、memoryview
和array.array
,这类序列只能容纳一种类型。
容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列里存放的是值而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。
序列类型(按修改与否):
- 可变序列
list
、bytearray
、array.array
、collections.deque
和memoryview
- 不可变序列
tuple
、str
和bytes
# tuple是容器序列,存放的是对任意类型的对象的引用
t = ("nightwatchmans", 5, {"sayhi": "hello word"})
print(t[2]["sayhi"]) # hello word
t[2]["sayhi"] = "hello china" # 可以修改的原因是,此处存放放是对字典的引用
print(t[2]["sayhi"]) # hello china
t[1] = 6 # TypeError: 'tuple' object does not support item assignment
2.2 列表推导和生成器表达式
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。
2.2.1 列表推导和可读性
#示例 1 把一个字符串变成Unicode 码位的列表
symbols = '$¢£¥€¤'
codes = []
for symbol in symbols:
codes.append(ord(symbol)) # ord(),返回字符对应的 ASCII 数值
print(codes) # [36, 162, 163, 165, 8364, 164]
用列表推导实现:
#示例 2 把字符串变成Unicode 码位的另外一种写法
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
print(codes) # [36, 162, 163, 165, 8364, 164]
句法提示
Python 会忽略代码里[]、{} 和() 中的换行,因此如果你的代码里有多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符\。
列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导,在Python 3 中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量还可以被正常引用,局部变量并不会影响到它们。
x = 'ABC'
dummy = [ord(x) for x in x]
print(x) # ABC
print(dummy) # [65, 66, 67]
2.2.2 列表推导同filter和map的比较
#示例 3 用列表推导和map/filter 组合来创建同样的表单
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
print(beyond_ascii) # [162, 163, 165, 8364, 164]
beyond_ascii = list(filter(lambda c: c > 127, map(ord, symbols)))
print(beyond_ascii) # [162, 163, 165, 8364, 164]
2.2.3 笛卡儿积
笛卡儿积是一个列表,列表里的元素是由输入的可迭代类型的元素对构成的元组,因此笛卡儿积列表的长度等于输入变量的长度的乘积。
# 示例 4 使用列表推导计算笛卡儿积
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = [(color, size) for color in colors for size in sizes] # 与两个for循环的嵌套关系结果一直,包括先后顺序
print(tshirts) # [('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
2.2.4 生成器表达式
虽然也可以用列表推导来初始化元组、数组或其他序列类型,但是生成器表达式是更好的选择。
- 生成器表达式背后遵守了迭代器协议,可以逐个地产出元素,能够节省内存。而不是建立一个完整的列表,然后再把这个列表传递到某个构造函数里。
- 生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。
与示例4 不同的是,用到生成器表达式之后,内存里不会留下一个有6个组合的列表,因为生成器表达式会在每次for 循环运行时才生成一个组合。
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
for tshirt in (f"{c} {s}" for c in colors for s in sizes):
print(tshirt) # black S ··· white L
2.3 元组不仅仅是不可变的列表
元组不仅仅可以当做“不可变列表”,还可以用于没有字段名的记录。
2.3.1 元组和记录
# 示例 7 把元组用作记录
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
for country, _ in traveler_ids: #for循环可以分别提取元组里的元素,也叫作拆包。元组中第二个元素无用,所以赋值给“_”占位符。
print(country) # USA BRA ESP
2.3.2 元组拆包
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用*
来表示忽略多余的元素。
元组拆包的应用:city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
和print('%s/%s' % passport)
。
# 拆包的应用示例
# 不使用中间变量交换两个变量的值:
a = "python"
b = "java"
b, a = a, b
print(a,b) # java python
# 用* 运算符把一个可迭代对象拆开作为函数的参数:
# divmod() 函数把除数和余数运算结果结合起来,返回一个包含商和余数的元组 (a // b, a % b)。
print(divmod(20, 8)) # (2, 4)
t = (20, 8)
print(divmod(*t)) # (2, 4)
quotient, remainder = divmod(*t)
print(quotient, remainder) # 2 4
用*
来处理剩下的元素
- 函数用*args 来获取不确定数量的参数
- 用在平行赋值中
# 在平行赋值中的运用:
a, b, *rest = range(5)
print(a, b, rest) # 0 1 [2, 3, 4]
a, b, *rest = range(2)
print(a, b, rest) # 0 1 []
a, *body, c, d = range(5) # 在平行赋值中,* 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置。
print(a, body, c, d) # 0 [1, 2] 3 4
2.3.3 嵌套元组拆包
接受表达式的元组可以是嵌套式的,例如(a, b, (c, d))。
# 示例 8 用嵌套元组来获取经度
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}'
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
2.3.4 具名元组 namedtuple
元组已经设计得很好用了,但作为记录来用的话,还是少了一个功能:我们时常会需要给记录中的字段命名。namedtuple函数的出现帮我们解决了这个问题。
用namedtuple 构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为Python 不会用__dict__
来存放这些实例的属性。
# 示例 9 定义和使用具名元组
import collections
City = collections.namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
print(tokyo) # City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
print(tokyo.coordinates) # (35.689722, 139.691667)
print(tokyo[1]) # JP
# 示例 10 具名元组的属性和方法
print(City._fields) # ('name', 'country', 'population', 'coordinates')
print(tokyo._fields) # ('name', 'country', 'population', 'coordinates')
LatLong = collections.namedtuple('LatLong', 'lat long')
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
delhi = City._make(delhi_data) # City(name='Delhi NCR', country='IN', population=21.935, coordinates=LatLong(lat=28.613889, long=77.208889))
delhi_test = City(delhi_data) # TypeError: <lambda>() missing 3 required positional arguments: 'country', 'population', and 'coordinates'
delhi._asdict() # {'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)
namedtuple 具名元组的特有属性
- 类属性
_fields
:包含这个类所有字段名的元组。 - 类方法
_make(iterable)
:接受一个可迭代对象来生产这个类的实例,注意City._make(delhi_data)
与City(delhi_data)
区别,前者是delhi_data拆包分给每个字段,后者是将delhi_data作为一个字段的值。 - 实例方法
_asdict()
:把具名元组以 collections.OrdereDict 的形式返回,可以利用它来把元组里的信息友好的展示出来。
2.3.5 作为不可变列表的元组
2.4 切片
在Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作。本节主要讨论的是高级切片形式的用法,它们的实现方法则会在第10 章的一个自定义类里提到。
2.4.1 为什么切片和区间会忽略最后一个元素
- 可以快速看出切片和区间里有几个元素:
range(3)
和my_list[:3]
都返回3 个元素。 - 可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
- 可以利用任意一个下标来把序列分割成不重叠的两部分,如
my_list[:x]
和my_list[x:]
。
2.4.2 对对象进行切片
用s[a:b:c]
的形式对 s 在 a 和 b 之间以 c 为间隔取值,即seq[start:stop:step]
。
s = 'bicycle'
print(s[::-1]) # elcycib
print(s[::-2]) # eccb
# 示例 11 纯文本文件形式的收据以一行字符串的形式被解析
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) # slice() 函数实现切片对象,主要用在切片操作函数里的参数传递。 class slice(stop),class slice(start, stop[, step])
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 Panavise Jr. - PV-201
$34.95 PiTFT Mini Kit 320x240
2.4.3 多维切片和省略
[]
运算符里还可以使用以逗号分开的多个索引或者是切片,外部库NumPy 就用到了这个特性,二维的numpy.ndarray 就可以用a[i, j]
这种形式来获取,抑或是用a[m:n, k:l]
的方式来得到二维切片。- 要得到
a[i, j]
的值,Python 会调用a.__getitem__((i, j))
。 - 省略(ellipsis)的正确书写方法是三个英语句号(…)。比如x 是四维数组,那么
x[i, ...]
就是x[i, :, :, :]
的缩写。
2.4.4 给切片赋值
l = list(range(10))
print(l) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l[2:5] = [20, 30]
print(l) # [0, 1, 20, 30, 5, 6, 7, 8, 9]
del l[5:7]
print(l) # [0, 1, 20, 30, 5, 8, 9]
l[3::2] = [11, 22]
print(l) # [0, 1, 20, 11, 5, 22, 9]
# 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。
l[2:5] = 100 #TypeError: can only assign an iterable
l[2:5] = [100] # [0, 1, 100, 22, 9]
print(l)
2.5 对序列使用+和*
Python 会默认序列是支持+ 和* 操作的。
+
和*
都遵循的规律:不修改原有的操作对象,而是构建一个全新的序列。序列乘以一个整数,同样会产生一个新序列。
如果在a * n 这个语句中,序列a 里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。
sizes = ['S', 'M', 'L']
sizes_repeat = [sizes] * 3
print(sizes_repeat) # [['S', 'M', 'L'], ['S', 'M', 'L'], ['S', 'M', 'L']]
sizes[0] = "xs"
print(sizes_repeat) # [['xs', 'M', 'L'], ['xs', 'M', 'L'], ['xs', 'M', 'L']]
建立由列表组成的列表
board = [['_'] * 3 for i in range(3)]
print(board) # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = 'X'
print(board) # [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
weird_board = [['_'] * 3] * 3
print(weird_board) # [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2] = 'O'
print(weird_board) # [['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
2.6 序列的增量赋值
+=
背后的特殊方法是__iadd__
(用于“就地加法”),同时对可变序列( 例如list、bytearray 和array.array)来说,a
会就地改动,就像调用了a.extend(b)
一样。(不新建对象)- 如果一个类没有实现这个方法的话,Python 会退一步调用
__add__
。a += b
这个表达式的效果就变得跟a = a + b
一样,首先计算a + b
,得到一个新的对象,然后赋值给a
。 - 上面所说的这些关于
+=
的概念也适用于*=
,不同的是,后者相对应的是__imul__
。 - 总体来讲,可变序列一般都实现了
__iadd__
方法,因此+=
是就地加法。而不可变序列根本就不支持这个操作,对这个方法的实现也就无从谈起。
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
str 是一个例外,因为对字符串做+= 实在是太普遍了,所以CPython 对它做了优化。为str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。
l = [1, 2, 3]
print(id(l)) # 2373173064320
l *= 2
print(id(l)) # 2373173064320 list是可变对象,就地乘法,未新建对象
t = (1, 2, 3)
print(id(t)) # 2373175906496
t *= 2
print(id(t)) # 2373173406016 tuple是不可变对象,t *= 2 等同于 t = t + t,新建对象
一个关于+=的谜题
t = (1, 2, [30, 40])
t[2] += [50, 60] # TypeError: 'tuple' object does not support item assignment
print(t)
# 示例 16 s[a] = b 背后的字节码
import dis
dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
2 LOAD_NAME 1 (a)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_NAME 2 (b)
10 INPLACE_ADD
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
这其实是个非常罕见的边界情况,总结如下:
- 不要把可变对象放在元组里面。
- 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
- 查看Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。
2.7 list.sort方法和内置函数sorted
- list.sort() 方法
就地排序列表,不会复制原列表,该方法的返回值是None,提醒你它病不会新建一个列表。 - 内置函数 sorted
新建一个列表作为返回值,可以接受任何形式的可迭代对象作为参数(包括不可变序列或生成器)。
返回None 其实是Python 的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None,代表传入的参数发生了变动,并且未产生新的对象。random.shuffle 函数也遵守了这个惯例。
2.8 用bisect来管理已排序的序列
2.8.1 用bisect来搜索
bisect 数组二分查找算法
对有序列表提供支持,使用基本的二分(bisection)算法,使得该有序列表可以在插入新数据仍然保持有序。
bisect.bisect_left(a, x, lo=0, hi=len(a))
在 a 中找到 x 合适的插入点以维持有序。参数lo 和hi用来缩小搜寻的范围,默认情况下整个列表都会被使用。如果 x 已经在 a 里存在,那么插入点会在已存在元素之前(也就是左边)。如果 a 是列表(list)的话,返回值是可以被放在 list.insert()
的第一个参数的。
# 示例 17 在有序序列中用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): # reversed 函数返回一个反转的迭代器。
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
2.8.2 用bisect.insort插入新元素
# 示例 19 insort 可以保持有序序列的顺序
import bisect
import random
SIZE=7
random.seed(1729) # seed()方法改变随机数生成器的种子,可以在调用其他随机模块函数之前调用此函数
my_list = []
for i in range(SIZE):
new_item = random.randrange(SIZE*2) # random.randrange ([start,] stop [,step]) 方法返回指定递增基数集合中的一个随机数,基数默认值为1。
bisect.insort(my_list, new_item)
print(f'{new_item:2,d} -> {my_list}')
字符串排序
import bisect
# my_list = ['b','c','a', 4, 'aa'] # TypeError: '<' not supported between instances of 'int' and 'str'
my_list = ['b','c','a', 'aa']
my_list.sort()
print(my_list) # ['a', 'aa', 'b', 'c']
bisect.insort(my_list, 'b')
print(my_list) # ['a', 'aa', 'b', 'b', 'c']
2.9 当列表不是首选时
2.9.1 数组
对于只包含数字的列表,array.array
比list
更高效。数组支持所有跟可变序列有关的操作,包括.pop
、.insert
和.extend
。另外,数组还提供从文件读取和存入文件的更快的方法,如.frombytes
和.tofile
。
array.fromfile
从一个二进制文件里读出1000 万个双精度浮点数只需要0.1 秒,比从文本文件里读取的速度要快60 倍,因为后者会使用内置的float 方法把每一行文字转换成浮点数。array.tofile
写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快7倍。- 1000 万个这样的数在二进制文件里只占用80 000 000 个字节(每个浮点数占用8 个字节,不需要任何额外空间),如果是文本文件的话,我们需要181 515 739 个字节。
# 示例 20 一个浮点型数组的创建、存入文件和从文件读取的过程
from array import array
from random import random
floats = array('d', (random() for i in range(10**7)))
print(floats[-1]) # 0.038179711931210614
fp = open('floats.txt', 'wb')
floats.tofile(fp)
fp.close()
floats2 = array('d')
fp = open('floats.txt', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()
print(floats2[-1]) # 0.038179711931210614
print(floats2 == floats) # True
2.9.2 内存视图
memoryview
是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。memoryview.cast
会把同一块内存里的内容打包成一个全新的memoryview
对象给你。
2.9.3 NumPy和SciPy
NumPy 实现了多维同质数组和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。通过NumPy,用户能对这些数据结构里的元素进行高效的操作。
SciPy 是基于NumPy 的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。
2.9.4 双向队列和其他形式的队列
利用.append
和.pop
方法,我们可以把列表当作栈或者队列来用,模拟栈的“先进先出”的特点。但是删除列表的第一个元素
(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
collections.deque
类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。
append
和popleft
都是原子操作,也就说是deque
可以在多线程程序中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。
除了deque
之外,还有些其他的Python 标准库也有对队列的实现。queue
、multiprocessing
、asyncio
、heapq
。
2.10 本章小结
- 扁平序列的体积更小、速度更快而且用起来更简单,但是它只能保存一些原子性的数据,比如数字、字符和字节。
- 容器序列则比较灵活,但是当容器序列遇到可变对象时,就需要格外小心了,因为时常会搞出一些“意外”。
2.11 延伸阅读
“优雅是简约之父”(“Elegance begets simplicity”)是2009 年在芝加哥的PyCon 的口号。
杂 谈
key 参数很妙
# key 参数能对一个混有数字字符和数值的列表进行排序,只需要是看作数值还是字符:
l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19]
print(sorted(l, key=int)) # [0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
print(sorted(l, key=str)) # [0, '1', 14, 19, '23', 28, '28', 5, 6, '9']