本文主要是 Fluent Python 第 2 章的学习笔记。这部分主要是介绍了序列、列表、元组的一些高级用法。


《Fluent Python》学习笔记:第 2 章 序列组成的数组

  • 2.1 内置序列分类
  • 2.2 列表推导和生成器表达式
  • 2.2.1 列表推导和可读性
  • 2.2. 列表推导式 VS filter and map
  • 2.2.3 笛卡尔积
  • 2.2.4 生成器表达式
  • 2.3 元组不仅仅是不可变列表
  • 2.3.1 元组和记录
  • 2.3.2 元组拆包
  • 2.4 切片
  • 2.5 序列拼接(+和\*)
  • 2.6 序列的增量赋值
  • 2.7 list.sort 方法和内置函数 sorted
  • 2.8 用 bisect 管理已排序的序列
  • 2.9 当列表不是首选时
  • 2.10 本章小结
  • 2.11 扩展阅读
  • 2.12 Luciano 的一些杂谈
  • 巨人的肩膀


2.1 内置序列分类

序列(sequence)分类,按照存放类型分类:

  1. 容器序列(container sequences):list, tuple, 和 collections.deue 能存放不同类型的数据。因为它们存放的是任意类型对象的引用。
  2. 平面序列(flat sequences): str, bytes, bytearray, memoryview 和 array.array 只能放一种数据类型。因为它们是在连续的内存空间上存放每个元素的值。

序列(sequence)分类,按照是否可变(mutability)分类:

  1. 可变序列(mutable sequences):list, bytearray, array.array, collections.deque, 和 memoryview.
  2. 不可变序列(immutable sequences):tuple, str, 和 bytes.

2.2 列表推导和生成器表达式

列表推导(list comprehensions, listcomps):构建 list 的快捷方式。
生成器表达式(generator expression, genexps):创建任何类型的序列的快捷方式。
它们的优点在于 可读性更好(more readable)经常更高效(often faster)

2.2.1 列表推导和可读性

通过对比“把一个字符串变为 Unicode 码位的列表”的两种实现方式,看看列表推导的可读性(readability)

# 常规方式
symbols = 'abcdef'
codes = []
for symbol in symbols:
    codes.append(ord(symbol))
print(codes)
[97, 98, 99, 100, 101, 102]
# 用列表推导
symbols = 'abcdef'
codes = [ord(symbol) for symbol in symbols]
print(codes)
[97, 98, 99, 100, 101, 102]

什么时候用列表推导式呢?

  1. 只用列表推导式创建新的列表,并且尽量保持简单。
  2. 如果列表推导式的代码超过两行,就要考虑用 for 循环重写。

列表推导式相比较于 for 循环还有一个优势在于,可以避免变量泄漏(leak variable)。注意:在 Python 2.x 版本存在变量泄漏问题。 Python 3 中不存在了。
举个栗子:

x = 'my precious'
ls = [x for x in 'abc']
print(x)  # output: my precious

x = 'my precious'
ls_2 = []
for x in 'abc':
    ls_2.append(x)
print(x)  # output: c
my precious
c

注意我这里用的是 Python 3.7 测试的,可以发现用 for 循环会修改变量 x 的值,导致变量泄漏。而用列表推导式就不会导致变量泄漏,原因是列表推导式(以及生成器表达式、集合推导式、字典推导式)在 Python 3 中都有自己的局部作用域,就像函数一样,表达式内部的变量和赋值,只作用于局部,不会影响推导式外面的变量和赋值。

2.2. 列表推导式 VS filter and map

列表推导式通过对序列或者其他可迭代类型进行过滤和加工操作,从而生成新的列表。内置的 filter 和 map 函数也是做这个的,两者有什么异同呢?

  1. 列表推导式可读性更强。
  2. 二者的运行速度不一定谁快谁慢。

看下面这个栗子:

# 列表推导式
symbols = 'abcdef'
codes = [ord(symbol) for symbol in symbols if ord(symbol) > 100]
print(codes)

# filter 和 map
symbols = 'abcdef'
codes = list(filter(lambda x: x > 100, map(ord, symbols)))
print(codes)
[101, 102]
[101, 102]

可以看到,filter 和 map 能做的事情,列表推导式也能做,而且可读性更好。运行速度,它们得看情况。

2.2.3 笛卡尔积

列表推导式可以使用多个嵌套的 for 循环,如使用列表推导式计算笛卡尔积。

# 用列表推导式
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
tshirts = [(color, size) for color in colors for size in sizes]
print(tshirts)

# 用嵌套 for 循环
for color in colors:
    for size in sizes:
        print((color, size))
[('black', 'S'), ('black', 'M'), ('black', 'L'), ('white', 'S'), ('white', 'M'), ('white', 'L')]
('black', 'S')
('black', 'M')
('black', 'L')
('white', 'S')
('white', 'M')
('white', 'L')

对比可以发现:

  1. 这里得到的结果是先以颜色排列,再以尺码排列。
  2. 列表推导式中的 2 个 for 语句顺序和用 for 循环嵌套实现的顺序一样。
  3. 如果想实现先按尺码排列,在以颜色排列,只需要把两个 for 子句换一下位置。

2.2.4 生成器表达式

列表推导式的作用是用来生成列表,虽然列表推导式也可以初始化元组、数组或其他序列类型,但是这时选择生成器表达式(generator expressions, genexp)是更好,因为它更节省内存。具体原因是:生成器表达式遵循迭代器协议,是惰性计算,需要的时候一个一个生成元素;列表推导式是先创建一个完整的列表,然后把这个列表传递到某个构造函数。
生成器表达式的语法和列表推导式一样,只是把方括号或者圆括号。
下面通过两个栗子体验一下,生成器表达式初始化除列表之外的序列。

# 用生成器表达式初始化元组
symbols = 'abcdef'
# 如果生成器表达式是一个函数调用过程中的唯一参数,
# 那么不需要额外用圆括号把它括起来
codes = tuple(ord(symbol) for symbol in symbols)
print(codes)

# 用生成器表达式初始化数组
import array
# array 构造方法需要两个参数,因此括号是必须的。
# array 构造方法的第一个参数制定了数组中数字的存储方式
print(array.array('I', (ord(symbol) for symbol in symbols)))

# 生成器表达式计算笛卡尔积
colors = ['black', 'white']
sizes = ['S', 'M', 'L']
# 生成器会逐个产生元素,不会一次性生成含有6个T恤样式的列表,
# 从而节省内存
for tshirt in (f"{c} {s}" for c in colors for s in sizes):
    print(tshirt)
(97, 98, 99, 100, 101, 102)
array('I', [97, 98, 99, 100, 101, 102])
black S
black M
black L
white S
white M
white L

2.3 元组不仅仅是不可变列表

元组(tuple)实际上有两个作用:

  1. 作为不可变的列表。
  2. 用于没有字段名的记录。

2.3.1 元组和记录

元组其实是对数据的记录(record):元组中的每个元素都存放了记录中的一个字段的数据,外加这个字段(field)的位置。

2.3.2 元组拆包

元组拆包(tuple unpacking)可以应用到任何可迭代对象上,唯一的硬性要求是:被迭代对象中的元素数量必须要跟接受这些元素的元组的空挡数一样,或者用 * 来表示忽略多余的元素。现在可迭代元素拆包(iterable unpacking)慢慢流行被接受。
元组拆包应用:

  1. 多变量同时赋值。
  2. 交换变量值。
  3. 函数返回多个值。
  4. 对于不需要的数据,可以用占位符 _ 处理。
  5. * 也可以处理多余的元素。
a, b = 12, 13
print(a, b)
a, b = b, a  # 元组拆包
print(a, b)

import os
# _ 做占位符,丢弃不要的数据
_, filename = os.path.split('/home/Jock/.ssh/idrsa.pub')
print(filename)

# * 处理剩余元素
a, b, *rest = range(1, 5)
print(f"rest:{rest}")
*head, a, b = range(1, 3)
print(f"rest:{head}")
a, *body, b = range(1, 5)
print(f"body: {body}")

## 嵌套元组拆包
t = (1, 2, 3, (4, 5))
a, b, c, (d, e) = t
print(f"d: {d}")
print(f"e: {e}")
12 13
13 12
idrsa.pub
rest:[3, 4]
rest:[]
body: [2, 3]
d: 4
e: 5

2.4 切片

Python 中 list、tuple、str 都支持切片(slicing)操作。

Python 中为什么切片和区间操作不包含区间范围内的最后一个元素?原因及好处:

  1. 这个习惯符合 Python、C 和其他语言下标以 0 开始的传统。
  2. 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)和 my_list[:3]都返回 3 个元素。
  3. 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,即:用最后一个数减去第一个下标(stop - start)。
  4. 方便我们利用任意一个下标把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:]即可。

切片的作用:

  1. 提取序列中的内容。
  2. 就地修改可变序列。
# 切片赋值
l = list(range(10))
print(l)
l[2:5]=[20, 30]
print(l)
del l[5:7]
print(l)
l[3::2] = [11, 22]
print(l)
# 对切片赋值则右边也必须是一个可迭代对象
l[2:5] = 100
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 6, 7, 8, 9]
[0, 1, 20, 30, 5, 8, 9]
[0, 1, 20, 11, 5, 22, 9]



---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-16-79d0212c1cd1> in <module>
      9 print(l)
     10 # 对切片赋值则右边也必须是一个可迭代对象
---> 11 l[2:5] = 100


TypeError: can only assign an iterable

2.5 序列拼接(+和*)

+:+号两侧的序列由相同的数据类型构成,拼接过程中,两个被操作的序列不会被修改,Python 会新建一个包含同样类型数据的序列来作为拼接的结果。
*:重复一个序列,然后拼接成一个新的序列。

+* 都遵循不改变原有的操作序列,而是构建一个新的全新序列。

list_a = [1, 2, 3]
list_b = [4, 5, 6]
list_c = list_a +list_b
print(list_c)

print(list_a * 3)
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 1, 2, 3, 1, 2, 3]

注意由 * 引发的问题。

# 用 * 初始化嵌套列表,正确做法
board = [['_'] * 3 for i in range(3)]
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

这等价于下面的代码

board = []
for i in range(3):
    row = ['_'] * 3
    board.append(row)
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
# 用 * 初始化嵌套列表,错误做法
board = [['_'] * 3 ] * 3
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

可以发现错误的做法, 其实是在外面的列表包含了 3 个指向同一个列表的引用。即列表中的 3 个元素都是指向同一个列表对象。
这和下面的写法是等效的

# 错误做法
row = ['_'] * 3
board = []
for i in range(3):
    board.append(row)
print(board)
board[1][2] = 'X'
print(board)
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

2.6 序列的增量赋值

增量赋值运算符 +=*= 的表现取决于它们的第一个操作对象,+= 背后的特殊方法是 __iadd__ 用于就地加法,如果一个类没有实现这个方法,那么 Python 就会退一步调用,__add__
考虑这个简单表达式:a += b 如果 a 实现了 __iadd__ 方法,那么就会调用这个方法,同时对可变序列(list, set, array.array)来说,a 会就地改动。如果 a 没有实现 __iadd__ 方法,那么 a += b 的效果就和 a = a + b 一样:首先计算 a + b,得到一个新的对象,再赋值给 a。即在这个表达式中,变量名会不会被关联到新的对象中。完全取决于这个类型有没有实现 __iadd__ 方法。

总体而言,可变序列一般实现了这个 __iadd__ 方法,因此 += 就是就地加法,而不可变序列,根本不支持这个操作,对这个方法的操作就无从谈起。
所以写成 list_a += list_b 比 list_a = list_a + list_b 高效!

# 用 ls = ls + [i]
ls = [1, 2]
print(id(ls))
for i in range(3, 10):
    ls = ls + [i]
print(ls)
print(id(ls))

# 用 ls += [i]
print('-' * 20)
ls = [1, 2]
print(id(ls))
for i in range(3, 10):
    ls += [i]
print(ls)
print(id(ls))
3140336148296
[1, 2, 3, 4, 5, 6, 7, 8, 9]
3140336867720
--------------------
3140336148296
[1, 2, 3, 4, 5, 6, 7, 8, 9]
3140336148296

+= 的概念也适用于 *=,不同的是,*= 是通过 __imul__ 实现的。

# *= 效果举例
l = [1, 2, 3]
print(id(l))
l *= 2
print(l)
print(id(l))
print('-' * 20)
t = (1, 2, 3)
print(id(t))
t *= 2
print(t)
print(id(t))
3140336134728
[1, 2, 3, 1, 2, 3]
3140336134728
--------------------
3140336928072
(1, 2, 3, 1, 2, 3)
3140336653032

注意:对不可变序列进行重复拼接操作,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。
不过 str 是个例外,因为对字符串 += 操作太频繁了,所以 CPython 对它进行了优化, 为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。

# 一个有意思的关于 += 的谜题
t = (1, 2, [30, 40])
t[2] += [50, 60]  # 这里用 t[2].extend([50, 60]) 就不胡报错了
print(t)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-4-99689adb4d4a> in <module>
      1 # 一个有意思的关于 += 的谜题
      2 t = (1, 2, [30, 40])
----> 3 t[2] += [50, 60]  # 这里用 t[2].extend([50, 60]) 就不胡报错了
      4 print(t)


TypeError: 'tuple' object does not support item assignment
print(t)
import dis
print(dis.dis('s[a] += b'))
(1, 2, [30, 40, 50, 60])
  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
None
  1. 不要把可变对象放到元组里面。
  2. 增量赋值不是一个原子操作,我们刚才也看到了,它虽然抛出了异常,但是还是完成了操作。
  3. 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助!

注:原子操作(atomic operation):不会被线程调度机制打断的操作,即一旦开始,一定会执行完才会结束。

2.7 list.sort 方法和内置函数 sorted

list.sort 和 sorted 的异同:

相同点:都含有两个可选的仅限关键字参数(keyword-only arguments) keyreverse。其中 key 是一个只有一个参数的函数,这个函数会依次作用于序列的每一个元素,所得的结果将作为排序的关键字(sorting key),key 默认是 None,即恒等函数(identity function),也就是默认用元素自己的值排序。reverse 默认是 False,即升序,设定为 True 即为降序排列。

不同点: list.sort 是列表方法,对列表进行原地(in place)排序,返回值是 None。sorted 是内置函数,可以对所有可迭代对象进行排序,并将排序结果作为一个新的列表返回。

注:Python API 的一个约定是如果一个函数或者方法是原地改变对象,那么应该返回 None。这么做的目的是为了告诉调用者对象被原地改变了。这个约定的弊端是无法级联调用(cascade call)这些方法。而返回新对象的方法可以级联调用,从而形参连贯的接口(fluent interface)。

# list.sort 和 sorted 举例
fruits = ['gape', 'raspberry', 'apple', 'banana']
print('返回一个新的按字母排序的列表')
print(sorted(fruits))
print('原列表不变')
print(fruits)
print('返回一个新的按字母降序排列的列表')
print(sorted(fruits, reverse=True))
print('返回一个新的按长度排序的列表,,注意这里是稳定排序算法实现,即grape 和 apple 长度相同,所以它们的相对位置和原来列表里一样')
print(sorted(fruits, key=len))
print('返回一个新的按长度降序排序的列表,,注意这里是稳定排序算法实现,即 grape 和 apple 长度相同,所以它们的相对位置和原来列表里一样')
print(sorted(fruits, key=len, reverse=True))
print('原列表 fruits 一直不变')
print(fruits)
print('对原列表进行原地排序')
print(fruits.sort())  # 这里打印 None 返回值
print('打印原地排序后的列表')
print(fruits)
返回一个新的按字母排序的列表
['apple', 'banana', 'gape', 'raspberry']
原列表不变
['gape', 'raspberry', 'apple', 'banana']
返回一个新的按字母降序排列的列表
['raspberry', 'gape', 'banana', 'apple']
返回一个新的按长度排序的列表,,注意这里是稳定排序算法实现,即grape 和 apple 长度相同,所以它们的相对位置和原来列表里一样
['gape', 'apple', 'banana', 'raspberry']
返回一个新的按长度降序排序的列表,,注意这里是稳定排序算法实现,即 grape 和 apple 长度相同,所以它们的相对位置和原来列表里一样
['raspberry', 'banana', 'apple', 'gape']
原列表 fruits 一直不变
['gape', 'raspberry', 'apple', 'banana']
对原列表进行原地排序
None
打印原地排序后的列表
['apple', 'banana', 'gape', 'raspberry']

2.8 用 bisect 管理已排序的序列

bisect 模块中的 bisect 和 insort 都是利用二分查找算法来在有序序列中查找或插入元素。

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):
        # 用特定的 bisect 函数来计算元素应该出现的位置
        position = bisect_fn(HAYSTACK, needle)
        # 利用该位置来算出需要几个分隔符
        offset = position * '  |'
        # 把元素和其应该出现的位置打印出来
#         print(ROW_FMT.format(needle, position, offset))
        print(f'{needle:2d} @ {position:2d}    {offset} {needle:<2d}')

if __name__ == '__main__':

    # 根据命令的最后一个参数来选用 bisect 函数
    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

上面的输出,每一行都以 needle @ position (元素及其应该插入的位置)开始,并展示了该元素在原序列中的物理位置。

排序的代价很高,当我们得到一个有序序列,最好能够保持它的有序。
bisect.insort 方法就是用于插入新元素且使序列保持有序。

import bisect
import random

SIZE = 7

random.seed(1729)

my_list = []
for i in range(SIZE):
    new_item = random.randrange(SIZE*2)
    bisect.insort(my_list, new_item)
#     print('%2d ->' % new_item, my_list)
    print(f'{new_item:2d} -> {my_list}')
10 -> [10]
 0 -> [0, 10]
 6 -> [0, 6, 10]
 8 -> [0, 6, 8, 10]
 7 -> [0, 6, 7, 8, 10]
 2 -> [0, 2, 6, 7, 8, 10]
10 -> [0, 2, 6, 7, 8, 10, 10]

2.9 当列表不是首选时

列表非常灵活强大且简单。有时要考虑效率的话,列表可能不是更好的选择。比如存放 100 万个浮点数,数组(array)的效率要高很多,因为数组背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述,这和 C 语言中的数组一样。

  1. 如果需要频繁对序列做先进先出(FIFO)或者后进先出(LIFO)操作,双端队列(double-ended queue)deque 会更快。collection.deque 类是一个线程安全、可以快速从两端添加或者删除元素的数据类型。存放“最近用到的几个元素”deque 也非常有用。
  2. 对成员进行测试时,set 更合适。
  3. 如果只需要一个存储数字的列表,array.array 比 list 更加高效。
# Creating, saving, and loading a large array of floats
from array import array  # 导入 array 类型
from random import random

# 利用一个可迭代对象(这里是一个生成器表达式)创建一个双精度浮点数组(类型码'd')
floats = array('d', (random() for i in range(10**7)))
print(floats[-1])  # 打印数组最后一个元素
fp = open('floats.bin', 'wb')
# 把数组存入一个二进制文件
floats.tofile(fp)
fp.close()
floats2 = array('d')  # 新建一个双精度浮点空数组
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()
print(floats2[-1])  # 打印数组最后一个元素
print(floats2 == floats)  # 比较 floats2 与 floats 是否相等
0.051056611520245765
0.051056611520245765
True

二进制文件比文本文件的读取和存储都更加高效。

collection.deque 类是一个线程安全、可以快速从两端添加或者删除元素的数据类型。存放“最近用到的几个元素”deque 也非常有用。deque 对头尾部的操作进行了优化, 它的代价是对队列中的中间元素进行操作会慢一些。

deque 中 append 和 popleft 都是原子操作,可以在多线程中安全地当做 LIFO 队列使用,而不用使用锁。

from collections import deque

# maxlen 是一个可选参数,代表这个队列可以容纳的元素数量,而且一旦设定,这个属性就不能修改了。
dq = deque(range(10), maxlen=10)
print(dq)
# 队列的旋转操作,接受一个参数 n,当 n > 0时,队列的最右边的 n 个元素会被的移动到队列的左边。
# 当 n < 0 时,最左边的 n 个元素会被移动到右边。
dq.rotate(3)
print(dq)
dq.rotate(-4)
print(dq)
# 在队列的左端条件元素-1,因为此时队列已满(len(d) == d.maxlen())
# 因此在它的头部添加元素时,它的尾部元素会被删除。
# 因此下一行中,元素0被删除了
dq.appendleft(-1)
print(dq)
# dq 队列尾部添加3个元素,同时会把dq队列头部的-1, 1 和 2 删除
dq.extend([11, 22, 33])
print(dq)
# 在dq队列头部添加 4 个元素,注意extendleft(iter)是把iter里面的元素依次添加到dq队列头部
# 所以iter中的元素会逆序出现在dq队列,同时dq队列尾部的4个元素被删除
dq.extendleft([10, 20, 30, 40])
print(dq)
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

2.10 本章小结

  1. 序列可以分为可变和不可变序列,或者扁平序列和容器序列。扁平序列体积小、速度快、用起来更简单,但是只能保存原子性数据,如:数字、字符和字节。容器序列更加灵活,但是要注意容器序列中包含可变对象时需要小心。
  2. 列表推导式和生成器表达式对于创建和初始化序列非常强大。一定要学会,用上了会上瘾。
  3. 元组可以作为无名称字段的记录,也可以作为不可变的列表。作为记录时,元组拆包非常安全可靠,* 在元组拆包非常有用。
  4. 序列切片非常强大,也是 Python 最后欢迎的一个特性。多维切片和省略,已经利用切片赋值改变可变序列很多时候都被忽略了。
  5. 重复拼接,+=*= 在处理可变序列和不可变序列时是不同的,对于可变序列是原地拼接,对于不可变序列是生成新的序列。本质上取决于序列本身对特殊方法的实现。
  6. sort 方法和内置函数 sorted 都使用简单且灵活,因为它们提供了一个可选的仅限关键字参数 key,用于指定一个仅有一个参数的函数,这个函数将用于对序列的每一个元素处理,并把处理结果作为排序算法比较对象。对于一个排好序的序列,为保持有序状态,可以使用 bisect.insort 来插入元素,使用 bisect.bisect 来快速查找元素(二分查找算法)。
  7. collection.deque 灵活强大,线程安全,对于频繁进行头尾操作或者存放最近用的今个元素非常有用。不适合对中间元素频繁操作的场景。

2.11 扩展阅读

  1. Python 官网对于内置函数 sorted 和 list.sort 方法的更多高级用法,推荐阅读:Sorting HOW TO
  2. Python 大多数排序(sort 方法和 sorted 函数)用的都是 Timsort 排序算法,它是一种混和的稳定排序算法,它综合了归并排序(merge sort)和插入排序(insertion sort)。可以在很多现实场景中都表现很好。具体介绍可以参考 Wiki Timsort
  3. 更好的使用元组拆包平行赋值,可以参考:使用 *extra
  4. 更广泛的使用可迭代对象拆包的讨论和提议,可以参考 PEP 484:PEP 448 – Additional Unpacking Generalizations
  5. 关于 collections 的更多用法 collections — Container datatypes
  6. 关于为什么 range 和 slice 取下限,不取上限,可以看看这篇经典的文章:Why numbering should start at zero 这篇文章主要内容是:
    为什么 range 和 slice 取下限,不取上限?
    表示 2, 3, …, 12 序列,可以用以下四种表示。

a) 2 ≤ i < 13
b) 1 < i ≤ 12
c) 2 ≤ i ≤ 12
d) 1 < i < 13

  1. 因为上限和下限的差值就是所取序列的长度。所以 a 和 b 优于 c 和 d。
  2. 下限使用 < 以及上限使用 < 都比较丑陋。因此我们下限使用 ,上限使用 <。所以 a 优于 b。
  3. Mesa 编程语言对所有四个约定中的整数间隔都有特殊的表示法。 Mesa 的广泛经验表明,使用其他三个约定一直是笨拙和错误的来源,鉴于此经验,强烈建议 Mesa 程序员不要使用后三个可用功能。

由此引出第二个问题,为什么下标从 0 开始?

  1. 从 0 开始索引,那么通过下标,我们就可以知道有多少个元素在这个元素前面。比如:l[1],我们就知道这是取的第 2 个元素,它前面有 1 个元素。
  2. 编程语言惯例。

2.12 Luciano 的一些杂谈

  1. 列表虽然可以存放不同类型的数据,但是通常不这么做,因为这么做并没有什么好处,而且列表中的元素如果不能比较大小,则无法对列表进行排序。列表通常存放具有通用特性的元素。
  2. 元组恰好相反,元组常用来存放不同数据类型,因为元组的每个元素都是独立的,彼此不相关。
  3. list.sort、sorted、max 和 min 中可选参数 key 的设计非常棒!使用 key 会更加高效且简洁。简单是指你只需要定义一个只有一个参数的函数,用于排序即可。高效是指 key 的函数在每个元素上只会调用一次,而双参数比较函数则每次两两比较的时候都会被调用。(PS:这一点还不是特别理解)
  4. sorted 和 list.sort 背后的排序算法都是 Timsort,它是一种自适应算法,会根据原始数据的特点交替使用归并排序(merge sort)和插入排序(insertion sort)。在实际应用场景中这是非常有效的一种稳定排序算法。Timsort 在 2002 年开始在 CPython 中使用,2009 年起,开始在 Java 和 Android 中使用。