流畅的Python

doctest是 Python 的一个标准库,用来测试,通过模拟控制台对话来检验表达式求值是否正确。

如:

1
python3 -m doctest example_script.py

第一部分 序幕

第一章 Python 数据模型

1.1 一摞 Python 风格的纸牌

特殊方法 __len____getitem.

__len__ 支持 len() 操作:

1
2
def __len__(self):
return len(self._cards)

__getitem__ 支持 card[] 这样的数组操作:

1
2
def __getitem__(self, position):
return self._card[position]

1.2 如何使用特殊方法

特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用他们。

PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构体。

特殊方法的调用是隐式的,如 for i in x: 语句,背后其实用的是 iter(x), 而这个函数的背后则是 x.__iter__() 方法。

通过内置的函数(如 len、iter、str, 等) 来使用特殊方法是最好的选择。

不要随意添加特殊方法。

1.2.1 模拟数值类型

利用特殊方法,可以让自定义对象通过加号等运算符进行运算。

特殊方法 __repr____abs____add____mul__ 实现向量类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from math import hypot

class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y

def __repr__:
return 'Vector(%r, %r)' % (self.x, self.y)

def __abs__(self):
return hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))

def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

1.2.2 字符串表示形式

Python 的一个内置函数叫 repr(感觉为 representation), 其将一个对象用字符串的形式表达出来,也就是”字符串表示形式”. 其利用的是 __repr__ 特殊方法。

使用 %r 来获取对象各个属性的标准字符串表示形式。

__repr____str__ 的区别在于,后者是在 str() 函数被使用,或是在 print 函数打印一个对象的时候才被调用。
difference between str and repr in Python

1.2.3 算术运算符

通过 __add____mul__.

1.2.4 自定义的布尔值

为了判断一个值 x 为真还是为假,Python 会调用 bool(x), 这个函数只能返回 True 或者 False.

bool(x) 的背后是调用 x.__bool__() 的结果。如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__(), 若返回 0, 则为 false.

可以实现为:

1
2
def __bool__(self):
return bool(self.x or self.y)

1.3 特殊方法一览

Python 语言参考手册

1.4 为什么 len 不是普通方法

速度可以很快。

1.6 延伸阅读

Python 语言参考手册里的 “Data Model” 一章
Martelli 的 Stack Overflow 主页

特殊方法有时被称为魔术方法。

元对象协议,元对象指那些对建构语言本身来讲很重要的对象,协议可以看做接口。元对象协议和对象模型都是指建构核心语言的 API.

第二部分 数据结构

第2章 序列构成的数组

2.1 内置序列类型概览

  • 容器序列, list、tuple 和 collections.deque 这些序列能存放不同类型的数据
  • 扁平序列,str、bytes、bytearray、memoryview 和 array.array, 这些序列只能容纳一种类型

容器序列存放的是它们所包含的任意类型的对象的引用。

扁平序列存放的是值而不是引用,其为一段连续的内存空间。

  • 可变序列,list、bytearray、array.array、collections.deque 和 memoryview
  • 不可变序列,tuple、str 和 bytes

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

list comprehension(listcomps) 是构建列表的快捷方式。

生成器表达式(generator expression 简称为 genexps)则可以用来创建其他任何类型的序列。

2.2.1 列表推导和可读性

通常的原则是,只用列表推导来创建新的列表,而且尽量保持简短。

Python 会省略代码里 []{}() 中的换行。

列表推导不会再有变量泄露问题

列表推导和生成器表达式在 Python3 中都有局部作用域。

2.2.2 列表推导同 filter 和 map 的比较

2.2.3 笛卡尔积

笛卡尔积是一个列表,其长度等于输入变量长度的乘积,其元素为输入变量的一一组合而构成的元组。

如:

1
2
3
4
5
6
7
8
9
10
11
  >>> colors = ['black', 'white']
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [(color, size) for color in colors for size in sizes]
```
列表推导的作用只有一个:生成列表。
#### 2.2.4 生成器表达式
生成器表达式遵循迭代器原理,可以住个人产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。

生成器表达式的语法跟列表推导差不多,只是把方括号改为圆括号而已。

如果生成器表达式是一个函数调用工程中的唯一参数,那么不需要额外用括号把它围起来。

tuple(ord(symbol) for symbol in symbols)

1
生成笛卡尔积:

colors = [‘black’, ‘white’]
sizes = [‘S’, ‘M’, ‘L’]
for tshirt in (‘%s %s’ % (c, s) for c in colors for s in sizes):
print(tshirt)
1
2
3
4
5
6
7
8
9
10
11
12
### 2.3 元组不仅仅是不可变的列表
元组还可以用作没有字段名的记录。
#### 2.3.1 元组和记录
元组其实是对数据的记录: 元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置。

把元组当做一些字段的集合时,其数量和位置信息就非常重要。

如果在任何表达式里我们在元组内对元素排序,这些元素所携带的信息就会丢失,因为这些信息是跟它们的位置有关的。

for 循环可以分别提取元组里的元素,也叫做拆包(unpacking).
#### 2.3.2 元组拆包
最好辨认的元组拆包形式就是平行赋值:

lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates # 元组拆包
1
可以用 `*` 运算符把一个可迭代对象拆开作为函数的参数:

t = (20, 8)
divmod(*t)
1
2
3
4
5
`_` 占位符用于处理不感兴趣的数据。
##### 用 * 来处理剩下的元素
在 Python 中,函数使用 `*args` 来获取不确定数量的参数。

在 Python3 中,这个概念被扩展到了平行赋值中:

a, b, *rest = range(5)
a, b, rest
(0, 1, [2, 3, 4])
1
在平行赋值中,`*` 前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任何位置:

a, *body, c, d = range(5)
a, body, c, d
(0, [1, 2], 3, 4)
1
2
3
4
5
6
7
8
#### 2.3.3 嵌套元组拆包
接受表达式的元组可以是嵌套式的, 如: (a, b, (c, d)).

在 Python3 之前,元组可以作为形参放在函数声明中, 如:`def fn(a, (b, c), d):`, 然而 Python3 不再支持这种格式。
#### 2.3.4 具名元组
`collections.namedtuple` 函数用于构建一个带字段名的元组和一个有名字的类。

创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字,后者可以是有多个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的一个字符串。

from collections import namedtuple
City = namedtuple(‘City’, ‘name country population coordinates’)
tokyo = City(‘Tokyo’, ‘JP’, ‘36.933’, (35.68922, 139.691667))
tokyo
City(name=’Tokyo’, country=’JP’, population=36.933, coordinates=(35.689722, 139.691667))

tokyo.population
36.933

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
所以说第一个参数是显示在括号之前. 等号之前的才是真正的类。一般这两个取相同的值。

具名元组还有一些自己专有的属性, 最有用的有: `_fields` 类属性、类方法 `_make(iterable)` 和实例方法 `_asdict`(所以说给变量或者函数命名时首字母尽量不用下划线).
#### 2.3.5 作为不可变列表的元组
除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。
### 2.4 切片
#### 2.4.1 为什么切片和区间会忽略最后一个元素
这个习惯符合 Python, C 和其他语言里以 0 作为起始下标的传统。
#### 2.4.2 对对象进行切片
可以用 `s[a:b:c]` 的形式对 s 在 a 和 b 之间以 c 为间隔取值.

在对 `seq[start:stop:step]` 进行求值的时候,Python 会调用 `seq.__getitem__(slice(start, stop, step))`.

切片还有两个额外的功能: 多维切片和省略.
#### 2.4.3 多维切片和省略
`[]` 运算符里还可以使用以逗号分开的多个索引或者是切片.

要得到 `a[i, j]` 的值,Python 会调用 `a.__getitem__((i, j))`

省略(ellipsis)的正确书写方法是三个英语句号(...), 其实际上是 Ellipsis 对象的别名,而 Ellipsis 对象又是 ellipsis 类的单一实例。
#### 2.4.4 给切片赋值

l = list(range(10))
1
[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]
1
2
3
4
5
6
7
8
### 2.5 对序列使用 + 和 *
Python 程序员会默认序列是支持 `+` 和 `*` 操作的。

`+` 用于拼接.

`*` 用于复制多份。
#### 建立由列表组成的列表
用列表推导实现:

board = [[‘‘] * 3 for i in range(3)]
board
[[‘
‘, ‘‘, ‘‘], [‘‘, ‘‘, ‘‘], [‘‘, ‘‘, ‘‘]]
1
区分:

weird_borad = [[‘_’] * 3] * 3

其本质是一个对象的多个引用。
### 2.6 序列的增量赋值
增量赋值运算符 `+=` 和 `*=` 的表现取决于它们的第一个操作对象。

`+=` 背后的特殊方法是 `__iadd__`, 其会就地改动,意思就是不会先做加法得到一个对象,然后赋值给左值.

如果一个类没有实现这个方法,Python 会退一步调用 `__add__`, 即先做加法创建一个对象用于赋值.

以上 `+=` 的概念也适用于 `*=`. 其对应的是 `__imul__`.

对不可变序列进行重复拼接操作效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后追加新的元素。
#### 一个关于 += 的谜题
3个教训:
  - 不要把可变对象放在元组里
  - 增量赋值不是一个原子操作
  - 查看 Python 的字节码对于了解代码背后的运行机制很有帮助
### 2.7 list.sort 方法和内置函数 sorted
`list.sort` 方法会就地排序列表,其返回值为 None.

Python 的一个惯例: 如果一个函数或者方法对对象进行的是就地改动,那它就应该返回 None.

`sorted` 函数会创建一个列表作为返回值。不管 `sorted` 接受的是何种参数,其最后都会返回一个列表。

这两个都有两个可选的关键词参数:
  - reverse, 设定为 True, 被排序的序列里的元素会以降序输出,其默认值为 False.
  - key, 其为一个只有一个参数的函数,会作用到序列中的每一个元素上,产生的结果用作排序的对比关键词即依据。如 `key=len` 则使用字符串的长度排序。其默认值为恒等函数(identity function), 也就是默认用元素自己本身的值来排序。
### 2.8 用 bisect 来管理已排序的序列
bisect 模块包含两个主要函数,bisect 和 insort, 两个函数都利用二分查找算法来在有序序列中查找或插入元素。
#### 2.8.1 用 bisect 来搜索
其实就是用来找位置,即 index.

`bisect(haystack, needle)`,在 haystack 里搜索可以放 needle 的位置,该位置满足的条件是,把 needle 插入这个位置之后,haystack 还能保持升序。其中 haystack 必须是一个有序的序列。

`bisect` 函数是 `bisect_right` 函数的别名,其姐妹函数叫 `bisect_left`, 它们区别在于 `bisect_left` 返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会放置于它相等的元素的前面,而 `bisect_right` 会放在后面。

`bisect` 函数有 lo 和 hi 两个可选参数。
#### 2.8.2 用 bisect.insert 插入新元素
`insort(seq, item)` 把变量 item 插入到序列 seq 中,并能保持 seq 的升序。
### 2.9 当列表不是首选时
#### 2.9.1 数组
若需要一个只包含数字的列表,`array.array` 比 `list` 更高效。

创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。

Python 不会允许你在数组里存放除指定类型之外的数据。
#### 2.9.2 内存视图
`memoryview` 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。

`memoryview.cast` 的概念跟数组模块类似,能用不同的方式读写同一块内存数据,而且内容字节不会随意移动。和 C 语言中的类型转换差不多。
#### 2.9.3 NumPy 和 SciPy
NumPy 和 SciPy 提供高阶数组和矩阵操作。

NumPy 实现了多维同质数组(homogeneous) 和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。

SciPy 是基于 NumPy 的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。SciPy 背后为 C 和 Fortran 代码。

流畅的Python
http://example.com/2022/08/22/流畅的Python/
作者
Jie
发布于
2022年8月22日
许可协议