流畅的Python
doctest
是 Python 的一个标准库,用来测试,通过模拟控制台对话来检验表达式求值是否正确。
如:
1 |
|
第一部分 序幕
第一章 Python 数据模型
1.1 一摞 Python 风格的纸牌
特殊方法 __len__
和 __getitem
.
__len__
支持 len()
操作:
1 |
|
__getitem__
支持 card[]
这样的数组操作:
1 |
|
1.2 如何使用特殊方法
特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用他们。
PyVarObject
是表示内存中长度可变的内置对象的 C 语言结构体。
特殊方法的调用是隐式的,如 for i in x:
语句,背后其实用的是 iter(x)
, 而这个函数的背后则是 x.__iter__()
方法。
通过内置的函数(如 len、iter、str, 等) 来使用特殊方法是最好的选择。
不要随意添加特殊方法。
1.2.1 模拟数值类型
利用特殊方法,可以让自定义对象通过加号等运算符进行运算。
特殊方法 __repr__
、__abs__
、__add__
、__mul__
实现向量类:
1 |
|
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 |
|
1.3 特殊方法一览
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 |
|
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 代码。