《Fluent Python》 CH.02_数据结构-序列构成的数组 读书笔记 (列表、元组、队列)
小结
- 计时,2021-01-30 21:30-23:30 加上 2021-01-31 19:30-21:30, 共计4小时
- python3 的ord,它以一个字符串(Unicode 字符)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值
- 转换文件的命令 jupyter nbconvert --to markdown E:\PycharmProjects\TianChiProject\00_山枫叶纷飞\competitions\013_fluent_python\CH.02_数据结构-序列构成的数组.ipynb
2.1 内置序列类型概览
Python 标准库用 C 实现了丰富的序列类型,列举如下。
序列类型还能按照存储数据类型来分类
容器序列
- list、tuple 和 collections.deque 这些序列能存放不同类型的 数据。
扁平序列
- str、bytes、bytearray、memoryview 和 array.array,这类 序列只能容纳一种类型。
容器序列存放的是它们所包含的任意类型的对象的引用,而扁平序列 里存放的是值而不是引用。
换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字 符、字节和数值这种基础类型。
序列类型还能按照能否被修改来进行分类
可变序列
- list、bytearray、array.array、collections.deque 和 memoryview。
不可变序列
- tuple、str 和 bytes。
2.2 列表推导和生成器表达式
列表推导是构建列表(list)的快捷方式,而生成器表达式则可以用来 创建其他任何类型的序列。
本节摘要:
- 列表推导不会再有变量泄漏的问题; Python 2.x中,在列表推导中 for 关键词之后的赋值操作可能会影 响列表推导上下文中的同名变量。在 Python 3 中是不 会出现的,列表推导、生成器表达式,以及同它们很相似的集合(set)推导 和字典(dict)推导,在 Python 3 中都有了自己的局部作用域,就 像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的 上下文里的同名变量还可以被正常引用,局部变量并不会影响到它 们。
2.2.2 列表推导同filter和map的比较
symbols = '$¢£¥€¤'
beyond_ascii = [ord(s) for s in symbols if ord(s) > 127]
beyond_ascii
[162, 163, 165, 8364, 164]
beyond_ascii2 = list(filter(lambda x:x>127, map(ord, symbols)))
beyond_ascii2
[162, 163, 165, 8364, 164]
2.2.3 列表表达式嵌套可以实现笛卡儿积
简,略
2.2.4 生成器表达式
优势:逐个产出元素,而不是先构建一个列表再转换数据到构造器函数中——节省一倍内存的占用,比较节省内存
用法:构造器内部for循环,同列表生成器
示例 2-5 用生成器表达式初始化元组和数组
symbols = '$¢£¥€¤'
tuple(ord(s) for s 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 元组不仅仅是不可变的列表
2.3.1 元组和记录 元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段 的数据,外加这个字段的位置。正是这个位置信息给数据赋予了意义。
- % 格式运算符能被匹配到对应的元组元素上。
- for 循环可以分别提取元组里的元素,也叫作拆包(unpacking)。因 为元组中第二个元素对我们没有什么用,所以它赋值给“_”占位符。
示例 2-7 把元组用作记录
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567')]
for passport in sorted(traveler_ids):
print('%s/%s' % passport)
for country, _ in traveler_ids:
print(country)
BRA/CE342567
USA/31195855
2.3.2 元组拆包
拆包:将一个整体拆成多个元素,应用到赋值或者函数的接受参数上。
可以用 * 运算符把一个可迭代对象拆开作为函数的参数
# 正常
lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates # 元组拆包
print(*(latitude, longitude))
33.9425 -118.408056
# divmod Return the tuple (x//y, x%y)
divmod(20, 8)
(2, 4)
t = divmod(20, 8)
quotient, remainder = divmod(*t)
print(quotient, remainder)
0 2
下面是另一个例子,这里元组拆包的用法则是让一个函数可以用元组的 形式返回多个值,然后调用函数的代码就能轻松地接受这些返回值。比 如 os.path.split() 函数就会返回以路径和最后一个文件名组成的元 组 (path, last_part):
import os
_, file_name = os.path.split('C:/1/2/3/3/my.file')
file_name
'my.file'
在Python中, 函数用 *args 来获取不确定数量的参数
在元组拆包中使用 * 也可以帮助我们把注意力集中在元组的 部分元素上。 用*来处理剩下的元素:
a,b, *rest = range(5)
a,b, rest
(0, 1, [2, 3, 4])
print('我又回去了~~')
a,b, *rest
我又回去了~~
(0, 1, 2, 3, 4)
2.3.3 嵌套元组拆包
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 具名元组 (工厂函数:快速一个基于元组的数据的类)
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类。
定义:
- 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个 字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空 格分隔开的字段名组成的字符串。
- 存放在对应字段里的数据要以一串参数的形式传入到构造函数中 (注意,元组的构造函数却只接受单一的可迭代对象)。
- 可以通过字段名或者位置来获取一个字段的信息。
示例:
from collections import namedtuple
City = namedtuple('city', ['name', 'country', 'population', 'coords'])
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
tokyo
city(name='Tokyo', country='JP', population=36.933, coords=(35.689722, 139.691667))
tokyo[0]
'Tokyo'
tokyo.name
'Tokyo'
2.3.5 作为不可变列表的元组
和list的区别
- 除了跟增减元素相关的方法之外,元组支 持列表的其他所有方法。
- 还有一个例外,元组没有 reversed 方 法,但是这个方法只是个优化而已,reversed(my_tuple) 这个用法在 没有 reversed 的情况下也是合法的。
2.4 切片
2.4.2 对对象进行切片
可以用 s[a🅱️c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值
- 注意点: a:b,表示遍历方向,逆向用b:a。
示例:
s = 'bicycle'
s[-1:0:-1]
'elcyci'
print('错误用法: ')
s[0:-1:-1]
错误用法:
''
2.4.3 多维切片和省略
[] 运算符里还可以使用以逗号分开的多个索引或者是切片,外部库 NumPy 里就用到了这个特性,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,抑或是用 a[m:n, k:l] 的方式来得到二维切片。
2.4.4 给切片赋值
如果把切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就 可以对序列进行嫁接、切除或就地修改操作。
用法注意点:
- ➊ 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代 对象。即便只有单独一个值,也要把它转换成可迭代的序列。eg: l[2:5] = [100]
通过下面这几个例子,你 应该就能体会到这些操作的强大功能:
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]
l[2:5] = [100] # 相当于把20, 30, 5转为100
l
[0, 1, 100, 8, 9]
2.5 对序列使用+和*
通常 + 号两侧的序列由 相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被 修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果。
l = [1, 2, 3]
l*5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
示例井字游戏板: 建立由列表组成的列表
board = [['_'] * 3 for i in range(3)]
board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = 'X'
board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
print('含有 3 个指向同一对象的引用的列表是毫无用处的; 等同于board.append()在外层执行了三次')
weird_board = [['_'] * 3] * 3
weird_board
含有 3 个指向同一对象的引用的列表是毫无用处的
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2]='O'
weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
2.6 序列的增量赋值(序列都是就地加法)
增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象。简单起见,我们把讨论集中在增量加法(+=)上,但是这些概念对 *= 和其他 增量运算符来说都是一样的。
+= 背后的特殊方法是 iadd (用于“就地加法”)。但是如果一个类 没有实现这个方法的话,Python 会退一步调用 add(先走加法计算--再重新赋值) 。
示例 2-14 一个关于+=的谜题
t = (1, 2, [30, 40])
t[2] += [50, 60]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-47-77e12e6150f0> in <module>
1 t = (1, 2, [30, 40])
----> 2 t[2] += [50, 60]
3
TypeError: 'tuple' object does not support item assignment
t
(1, 2, [30, 40, 50, 60])
关于+=的谜题结论:
- 虽然报错了,但是赋值更改却成功了
- 分析s[a] += b 背后的字节码,简略分为三步:
- 将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端)。
- 计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对 象(也就是示例 2-15 里的列表t = (1, 2, [30, 40]) )。
- s[a] = TOS 赋值。这一步失败,是因为 s 是不可变的元组(示例 2-15 中的元组 t)。
作者得出的3个教训
- 不要把可变对象放在元组里面。
- 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异 常,但还是完成了操作。
- 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机 制很有帮助。
2.7 list.sort方法和内置函数sorted
- 默认使用稳定排序Timsort,排序值相同则位置一定不会发生变化
- list.sort 方法会就地排序列表
- 内置函数 sorted,它会新建一个列表作为返回 值
不管是 list.sort 方法还是 sorted 函数,都有两个可选的关键字参 数:
- reverse
- 如果被设定为 True,被排序的序列里的元素会以降序输出(也就 是说把最大值当作最小值来排序)。这个参数的默认值是 False。
- key (对比关键字)
- 一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些 字符串排序时,可以用 key=str.lower 来实现忽略大小写的排序,或 者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒
等函数(identity function),也就是默认用元素自己的值来排序。 - 可选参数 key 还可以在内置函数 min() 和 max() 中起作用。 另外,还有些标准库里的函数也接受这个参数,像 itertools.groupby() 和 heapq.nlargest() 等。
- 一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些 字符串排序时,可以用 key=str.lower 来实现忽略大小写的排序,或 者是用 key=len 进行基于字符串长度的排序。这个参数的默认值是恒
示例如下:
fruits = ['grape', 'raspberry', 'apple', 'banana']
sorted(fruits)
fruits
['grape', 'raspberry', 'apple', 'banana']
sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry']
print('验证sorted之后结果没变: ')
fruits
['grape', 'raspberry', 'apple', 'banana']
2.8 用bisect来管理已排序的序列
2.8.1 用bisect来搜索
bisect(haystack, needle) 在 haystack(干草垛——升序列表)里搜索 needle(针——对应数值的指针下标)的位置,该位置满足的条件是,把 needle 插入这个位置 之后,haystack 还能保持升序。也就是在说这个函数返回的位置前面 的值,都小于或等于 needle 的值。其中 haystack 必须是一个有序的 序列。
你可以先用 bisect(haystack, needle) 查找位置 index,再 用 haystack.insert(index, needle) 来插入新值。但你也可用 insort 来一步到位,并且后者的速度更快一些。
bisect(二分查找)的参数调整有:
- lo 和 hi, 两个可选参数——lo 和 hi——来缩小搜寻的范围。lo 的默认值是 0,hi 的默认值是序列的长度,即 len() 作用于该序列的 返回值
- bisect 函数其实是 bisect_right 函数的别名,后者还有个姊 妹函数叫 bisect_left。它们的区别在于,bisect_left 返回的插入 位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放 置于它相等的元素的前面,而 bisect_right 返回的则是跟它相等的元 素之后的位置。
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 bisect_demo(bisect_fn):
for needle in reversed(NEEDLES):
position = bisect_fn(HAYSTACK, needle)
offset = position * ' |'
print(ROW_FMT.format(needle, position, offset))
print('正常bisect: ')
bisect_demo(bisect.bisect)
正常bisect:
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
"""
def bisect_left(a, x, lo=0, hi=None):
Return the index where to insert item x in list a, assuming a is sorted.
The return value i is such that all e in a[:i] have e < x, and all e in
a[i:] have e >= x. So if x already appears in the list, a.insert(x) will
insert just before the leftmost x already there.
Optional args lo (default 0) and hi (default len(a)) bound the
slice of a to be searched.
"""
print('bisect_left,遇见相同的数值,插入数据到原数值的左边: ')
bisect_demo(bisect.bisect_left)
bisect_left,遇见相同的数值,插入数据到左边:
31 @ 14 | | | | | | | | | | | | | |31
30 @ 13 | | | | | | | | | | | | |30
29 @ 12 | | | | | | | | | | | |29
23 @ 9 | | | | | | | | |23
22 @ 9 | | | | | | | | |22
10 @ 5 | | | | |10
8 @ 4 | | | |8
5 @ 2 | |5
2 @ 1 |2
1 @ 0 1
0 @ 0 0
2.8.2 用bisect.insort插入新元素
排序很耗时,因此在得到一个有序序列之后,我们最好能够保持它的有 序。bisect.insort 就是为了这个而存在的。 insort(seq, item) 把变量 item 插入到序列 seq 中,并能保持 seq 的升序顺序。
参数可选:
- insort 跟 bisect 一样,有 lo 和 hi 两个可选参数用来控制查找的范 围。它也有个变体叫 insort_left,这个变体在背后用的是 bisect_left。
- 就地修改,默认无返回值
import bisect
import random
arr = [1,2,3,4,5]
ret = bisect.insort(arr, 233)
# 默认无返回值
ret
arr
[1, 2, 3, 4, 5, 233]
2.9 当列表不是首选时
比如,要存放 1000 万个浮点数的话,数组(array)的效率要高 得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如 果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该 会更快。
2.9.1 数组
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更 高效。数组支持所有跟可变序列有关的操作,包括 .pop、.insert 和 .extend。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes 和 .tofile。
Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类 型码用来表示在底层的 C 语言应该存放怎样的数据类型。比如 b 类型码 代表的是有符号的字符(signed char),因此 array('b') 创建出的 数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列 很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存 放除指定类型之外的数据。
示例 2-20 展示了从创建一个有 1000 万个随机浮点数的数组开始,到如 何把这个数组存放到文件里,再到如何从文件读取这个数组。
示例 2-20 一个浮点型数组的创建、存入文件和从文件读取的过程
from array import array
from random import random
# d 就是double
float_arr = array('d', (random() for i in range(10**7)))
len(float_arr)
10000000
fp = open('../file/floats.bin', 'wb')
float_arr.tofile(fp)
fp.close()
floats2 = array('d')
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()
print(' 检查两个数组的内容是不是完全一样:')
float_arr == floats2
数据排序只能使用sorted 函数
从 Python 3.4 开始,数组类型不再支持诸如 list.sort() 这种就地 排序方法。要给数组排序的话,得用 sorted 函数新建一个数组:
sorted(float_arr)
2.9.2 内存视图 memoryview
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同 一个数组的不同切片。
内存视图其实是泛化和去数学化的 NumPy 数组。它让你在不需要 复制内容的前提下,在数据结构之间共享内存。其中数据结构可以 是任何形式,比如 PIL图片、SQLite 数据库和 NumPy 的数组,等 等。这个功能在处理大型数据集合的时候非常重要。
memoryview.cast
memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一 块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型 转换的概念差不多。memoryview.cast 会把同一块内存里的内容打包
成一个全新的 memoryview 对象给你。
示例 2-21 通过改变数组中的一个字节来更新数组里某个元素的值
# 'h'表示有符号整数
numbers = array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
memv
<memory at 0x0000015335E43648>
print(len(memv))
print(memv[0])
5
-2
2.9.3 NumPy和SciPy
NumPy 实现了多维同质数组(homogeneous array) 和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记 录。通过 NumPy,用户能对这些数据结构里的元素进行高效的操作
SciPy 是基于 NumPy 的另一个库,它提供了很多跟科学计算有关的算 法,专为线性代数、数值积分和统计学而设计。
2.9.4 双向队列和其他形式的队列
collections.deque 类(双向队列)
collections.deque是一个线程安全、可以快速从两 端添加或者删除元素的数据类型。
而且如果想要有一种数据类型来存 放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新 建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满 员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。示 例 2-23 中有几个双向队列的典型操作。
deque的参数说明:
- maxlen 是一个可选参数,代表这个队列可以容纳的元素的数量,而且一旦设定,这个属性就不能修改了.
- rotate 队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n 个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被 移动到右边。
- append、appendleft 进行添加
- extend、extendleft 按个按顺序添加序列的数据进去
- append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序 中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。
代码示例 2-23 使用双向队列:
from _collections import deque
dq = deque(range(10), maxlen=10)
dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
dq.extendleft([10, 20, 30, 40])
dq
deque([40, 30, 20, 10, 0, 1, 2, 3, 4, 5])
其他的队列
-
queue
- 提供了同步(线程安全)类 Queue、LifoQueue 和 PriorityQueue,不同的线程可以利用这些数据类型来交换信息。这三 个类的构造方法都有一个可选参数 maxsize,它接收正整数作为输入 值,用来限定队列的大小。但是在满员的时候,这些类不会扔掉旧的元 素来腾出位置。相反,如果队列满了,它就会被锁住,直到另外的线程 移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃 线程的数量。
-
multiprocessing(多进程)
- 不是线程, 线程是thread
- 这个包实现了自己的 Queue,它跟 queue.Queue 类似,是设计给 进程间通信用的。同时还有一个专门的 multiprocessing.JoinableQueue 类型,可以让任务管理变得更方 便。
-
asyncio (异步编程专用)
- Python 3.4 新提供的包,里面有 Queue、LifoQueue、PriorityQueue 和 JoinableQueue,这些类受 到 queue 和 multiprocessing 模块的影响,但是为异步编程里的任务 管理提供了专门的便利。
-
heapq
- 跟上面三个模块不同的是,heapq 没有队列类,而是提供了 heappush 和 heappop 方法,让用户可以把可变序列当作堆队列或者优 先队列来使用。

浙公网安备 33010602011771号