<流畅的 Python> 摘录
本文目录
- 前言
- 第 1 章 Python 数据模型
- 第 2 章 序列构成的数据组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽像基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第 14 章 可迭代的对象、迭代器和生成器
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 其它信息
前言
要不这样吧,如果编程语言里有个地方你弄不明白,而正好又有个人用了这个功能, 那就开枪把他打死。这比学习新特性要容易些,然后过不了多久,那些活下来的程序 员就会开始用 0.9.6 版的 Python,而且他们只需要使用这个版本中易于理解的那一 小部分就好了。 ———— Tim Peters,传奇的核心开发者,“Python 之禅” 作者
...... 人们总是倾向于寻求自己熟悉的东西。受到其他语言的影响,你大概能猜到 Python 会支持正则表达式,然后就会去查阅文档。但是如果你从没见过元组拆包,也没 听说过描述符这个概念,那么估计你也不会特地去搜索它们,然后就永远失去了使用这些 Python 独有的特性的机会 ...... 这本书并不是一本完备的技术手册,而是会强调 Python 作用编程语言独有的特性,这些特性或者是只有 Python 才具备的,或者是在其他大众语 言里很少见的。
第 1 章 Python 数据模型
Guido 对语言设计的美学的深入理解让人震惊。我认识不少很不错的编程语言设计者, 他们设计出来的东西确实很精彩,但是从来都不会有用户。Guido 知道如何在理论 上做出一定的妥协,设计出来的语言让使用者觉得如沐春风,这真是不可多得。 ———— Jim Hugunin, Jython 的作乾,AspectJ 的作者之一,.NET DLR 架构师
摘录
Python 最好的品质之一是一致性。
数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些 模块包括但不限于序列、迭代器、函数、类和上下文管理器。
Python 2 Data Model: https://docs.python.org/2/reference/datamodel.html Python 3 Data Model: https://docs.python.org/3/reference/datamodel.html - 列出了 83 个特殊方法,其中 47 个用于实现算术运算、位运算和比较操作 Python 文档里总是用 “Python 数据模型” 这种说法,而大多数作者提到这个概念的 时候会说 “Python 对象模型”。维基百科中对象模型的第一个定义 (http://en.wikipedia.org/wiki/Object_model) 是:计算机编程语言中对象的属 性。这正好是 “Python 数据模型” 所要描述的概念。
不管在哪种框架下写程序,都会花费大量时间去实现那些会被框架本身调用的方法, Python 也不例外。Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的 对象操作,这些特列方法的名字以两个下划线开头,以两个下划线结尾。
魔术方法(magic method)是特殊方法的昵称。(在本书中,作者将)特殊方法也称 为双下方法(dunder method)。
首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它 们 ...... 然而,如果是 Python 内置的内型,比如列表(list)、字符串(str)、字 节序列(bytearray)等,那么 CPython 会抄个近路, __len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。 PyVarObject 是表示内存中长度可变的 内置对象的 C 语言结构体。直接读取这个值比调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是 iter(x) ,而这个函数的背后则是 x.__iter__() 方法 ...... 通常你的代码无需 直接使用特殊方法。
通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表 达力的代码 --- 或者说,更具 Python 风格的代码。
Python 数据模型的特殊方法还有很多,本书会涵盖其中的绝大部分,探讨如何使用和实现 它们。
杂项
- __getitem__ 和 __len__
- 实现了 __len__ 方法,类实例就可以和标准 Python 集合类型一样,使用 len() 函数了;
- 仅仅实现了 __getitem__ 方法,类实例就变成可迭代的了,同时也支持反向迭代 (此时也需要定义 __len__ 方法);
- 迭代通常是隐式的,譬如一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按照顺序做一次迭代搜索;
- abs 是一个内置函数,如果输入是整数或者浮点数,它返回的是输入值的绝对值; 如果输入是复数,那么返回这个复数的模。这种一致性需要使用 __abs__ 来保证。
- Python 对象的一个基本要求就是它得有合理的字符串表示形式。Python 有一个内置的
函数叫 repr ,它能把一个对象用字符串的形式表达出来以便辨认, repr 就
是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。
- __repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建 出这个被打印的对象。
- __repr__ 和 __str__ 的区别在于,后者被 str 函数或 print 函 数中调用,并且返回的字符串对终端用户更友好。
- 如果你只想实现这两个特殊方法中的一个, __repr__ 是更好的选择,如果一个 对象未定义 __str__ 方法时,在需要使用它的场合中,解释器会转而使用 __repr__ 作为替化。
- __add__ 和 __mul__ 为类实例增加了 + 和 * 运算符。中缀运算符的 基本原则就是不改变操作数对象,而是产出一个新的实例。
- 默认情况下,我们自己定义的类的实例总被认为是真,除非这个类对 __bool__ 或
者 __len__ 方法有自己的实现: bool(x) 函数先尝试调用 x.__bool__() ;
如果 __bool__ 方法不存在,那么再尝试设用 x.__len__() 。
- __bool__ 是 Python 3.0+ 引入的特殊方法,Python 2 使用 __nonzero__
第 2 章 序列构成的数据组
你可能注意到了,之前提到的几个操作可以无差别地应用于文本、列表和表格上。我 们把文本、列表和表格叫作*行列*(trains)... FOR 命令通常能作用于行列上。 ———— Geurts、Meertens 和 Pemberton ABC Programmer's Handbook
摘录
在创造 Python 以前,Guido 曾为 ABC 语言贡献过代码。ABC 语言是一个致于于为初学者 设计编程环境的长达 10 年的研究项目,其中很多点子在现在看来都很有 Python 风格: 序列的泛型操作、内置的元组和映射类型、用缩进来架构的源码、无需变量声明的强类型 等等 ...... Python 也从 ABC 那里继承了用统一的风格去处理序列数据这一特点。不管是 哪种数据结构,字符串、列表、字节序列、数据、XML 元素,抑或是数据库查询结果,它 们都共用一套丰富的操作:迭代、切片、排序、还有拼接。
Python 标准库用 C 实现了丰富的序列类型,列举如下:
- 容器序列 - list, tuple, collections.deque
- 扁平序列 - str, bytes, bytearray, memoryview, array.array
容器序列 存放的是它们所包含的任意类型的对象的引用,而 扁平序列 里存放的是值 而不是引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更 加紧凑,但是它里面只能存放诸如字符、字节和数值这样的基础类型。
序列类型还能按照能否被修改来分类:
- 可变序列 - list, bytearray, array.array, collections.deque, memoryview
- 不可变序列 - str, tuple, bytes
列表
列表推导是构建列表的快捷方式,而生成器表达式则可以用来创建其他任何类型的序列。 如果你的代码里并不经常使用它们,那么很可能你错过了许多写出可读性更好且更高效的 代码的机会。另一方面,列表推导也可能被滥用,通常原则是,只用列表推导来创建新的 列表,并且尽量保持简短。如果列表推导的代码超过了两行,你可能就要考虑是不是得用 for 循环重写了 ...... 列表推导可以帮助我们把一个序列或是其它可迭代类型中的元素 过滤或是加工,然后再新建一个列表。Python 内置的 filter 和 map 函数组合 起来也能达到这一效果,但是可读性上打了不小的折扣 ...... 生成器表达式背后遵守了 迭代器协议,可以逐个地产生元素 ...... 能够节省内存。
tuple(ord(char) for char in "ABCD")
元组
元组(tuple)其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据, 外加这个字段的位置。正是这个位置信息给数据赋予了意义 ...... 元组可以充当成一个 不可变的列表(list),除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。
print 函数使用 % 运算符也是*元组拆包*的一种应用 ...... 元素拆包可以应 用到任何可迭代对象上,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档 数一致。除非我们用 * 来表示忽略多余的元素(Python 3+)
a, b, *rest = range(5) # Python 3+
a, *body, c, d = range(5) # Python 3+
a, b, (c, d) = (1, 2, (3, 4)) # Python 2+
collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一 个有名字的类 ...... 用 namedtuple 构建的类的实例所消耗的内存跟元组是一样的, 因为字段名都被存在对应的类里面,这个实例跟普通的对象实例比起来也要小一些,因为 Python 不会用 __dict__ 来存放这些实例的属性 ...... 可以通过字段名或者位置来 获取一个字段的信息 ...... 除了从普通元组那里继承来的属性之外, 具名元组 还有 一些自己专有的属性,比如: _fields 类属性、类方法 _make(iterable) 和 实例方法 _asdict 。
Card = collections.namedtuple("Card", ["rank", "suit"])
切片
在 Python 里,像列表、元组和字符串这类序列类型都支持切片操作 ...... 在切片和区间 操作里不包含区间的最后一个元素是 Python 的风格,这个习惯符合 Python、C和其它语 言里以 0 作为超始下标的传统。这样做带来的好处如下:
- 当只有最后一个位置信息时,我们可以快速看出切片和区间里有几个元素, 比如 my_list[:3] 和 range(3) 都有三个元素
- 当起止位置信息都可见时,我们可以快速计算出无片和区间的长度: stop - start
- 还可以使用任意一个下标把序分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了
...... 我们还可以用 s[a:b:c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。 c 的值还可以为负,负值意味着反向取值 ...... 对 seq[start:stop:step] 进行求值的时候,Python 实际上会调用 seq.__getitem__(slice(start, stop, step)) 。
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
[] 运算符里还可以使用以逗号分开的多个索引或者切片,外部库 NumPy 里就用到 了这个特性 ...... 要正确处理这种 [] 运算符的话,对象的特殊方法 __getitem__ 和 __setitem__ 需要以元组的形式来接收 a[i, j] 中的索引。 也就是说,如果要得到 a[i, j] 的值,Python 会调用 a.__getitem__((i, j)) ...... Python 内置的序列类型都是一维的,因此它们只支 持单一索引,成对出现的索引是没有用的。
如果发切片放在赋值语句的左边,或把它作为 del 操作的对象,我们就可以对可变序 列进行嫁接、切除或就地修改等操作:
>>> l = list(range(10))
>>> 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[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an interable
运算
Python 程序员会默认序列是支持 + 和 * 操作的。通常 + 号两帷幕的序列 由相同类型的数据所构成,在拼接的过程中,两个被操作的序列都不会被修改,Python 会 新建一个包含同样类型数据的序列来作为拼接的结果。如果想要把一个序列复制几份然后 再拼接起来,更快捷的做汉是把这个序列乘以一个整数。同样,这个操作会产生一个新序 列。
增量赋值运算符 += 和 *= 等等的表现取决于它们的第一个操作对象,我们使用 += 做为例子。 += 背后的特殊方法是 __iadd__ (用于“就地加法”)。但是 如果第一个操作数没有实现该方法的话,Python 会退一步调用 __add__ ...... 总体 来讲,可变序列一般都实现了 __iadd__ 方法,而不可变序列根本不支持“就地加法” ,也就不会实现 __iadd__ 。
一个关于 += 的謎题 :
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
排序
list.sort 方法会就地排序 ...... 返回值是 None ...... 提醒你本方法不会新 建一个列表。这种情况下返回 None 其实是 Python 的一个惯例:如果一个函数或者 方法对对象进行的是就地改动,那它就应该返回 None ,好让调用者知道传入的参数 发生了变动,而且并半产生新的对象 ...... 内置函数 sorted 会新建一个列表作为 返回值。这个函数可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成 器。而不管 sorted 接受的是怎样的参数,它最后都会返回一个列表。
己排序的序列可以用来进行快速搜索,而标准库的 bisect 模块给我们提供了二分查 找算法。
其它
虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组( array )的效率要高得多,因为数组在背后存的并不 是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言的数组 一样。再比如,如果需要频繁对序列做先进先出的操作, deque 的速度应该会更快。
创建数组时,需要指定需要存储的数据类型,Python 数组不允许该类型以外的数据添加到 数组中。如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高 效。数组支持所有跟可变序列有关的操作,包括, .pop 、 .insert 和 .extend 。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes 和 .tofile 。
memoryview (Python 3+)是一个内置类,它能让用户在不复制内容的情况下操作同 一个数组的不同切片。
...... 我们可以把列表当作栈或者队列来用。但是删除列表的第一个元素之类的操作是很 耗时的,因为这些操作会牵扯到移动列表里的所有元素 ...... collection.deque 类 是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数 据类型来存放 “最近用到的几个元素”, deque 也是一个很好的选择。这是因为在新 建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反 向端删除过期的元素,然后尾端添加新的元素 ...... 但是为了实现这些方法,双向队列 也付出了一些代价,从队列中间删除元素的操作会慢一些,因为它只对在头尾的操作进行 了优化 ...... deque.append 和 deque.popleft 都是原子操作,也就是说 deque 可以在多线程程序中安全地当作先进先出的栈使用 ......
除了 deque 之外,标准库也提供了对队列的实现:
- 线程安全的 queue.Queue 、 queue.LifoQueue 、 queue.PriorityQueue
- 可用于进程间通信的 multiprocessing.Queue
- Python 3.4 的 asyncio 包提供的 Queue 、 LifoQueue 、 PriorityQueue 和 JoinableQueue 等为异步编程里的任务管理提供了的便利
- 实现了堆排序算法的 heapq ,可以用作堆队列或者优化级队列
杂项
- Python 会忽略代码里 [], {}, 和 () 中的换行,因此如果你的代码里有 多行的列表、列表推导、生成器表达式、字典这一类的,可以省略不太好看的续行符 \ 。
- 生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而己;如果生成器 表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。
- Python 的高产贡献者 Raymond Hettinger 写了一个排序集合模块 sortedcollection ,其中集成了 bisect 功能,但是比独立的 bisect 更易 用。
- Python 入门教材往往会强调列表是可以同时容纳不同类型的元素的,但是实际上这样做 并没有什么特别的好处。元素则恰恰相反,它经常用来存放不同类型的元素。这些符合 它的本质,元线就是用作存放彼此之间没有关系的数据的记录。
- list.sort 和 sorted 背后的排序算法是 Timsort ,它是一种自适应算法, 会根据原始数据的顺序特点交替使用插入排序和归并排序,以达到最佳效率。这样的算 法被证明是很有效的,因为来自真实世界的数据通常是有一定的顺序特点的。
第 3 章 字典和集合
字典这个数据结构活跃在所有 Python 程序的背后,即便你的源码里并没有直接用到 它。 ———— A.M.Kuchling,<代码之美> 第 18 章 “Python 的字典类:如何打造全能战士”
摘录
dict 类型不便在各种程序里广泛使用,它也是 Python 语言的基石。模块的命令空间 、实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在 __builtins__.__dict__ 模块中 ...... 正是因为字典至关重要,Python 对它的实现 做了高度优化,而 散列表 则是字典类型性能出众的根本原因 ....... 集合( set )的实现其实也依赖散列表。
字典
collections.abc 模块中有 Mapping 和 MutableMapping 这两个抽像基类, 它们的作用是为 dict 和其他类似的类型定义 形式接口 (在 python 2.6 - python 3.2 的版本中,这些类还不属于 collections.abc 模块,而是隶属于 collections 模块 ...... 非抽像映射类型一般不会直接继承这些抽像基类,它们会 直接对 dict 或者 collections.UserDict 进行扩展。这些抽像基类的主要作用 是作为 形式化的文档 ,它们定义了构建一个映射类型所需要的最基本的接口。然后它 们还可以跟 isinstance 一起被用来判定某个数据是不是广义上的映射类型:
>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
标准库里的所有映射类型都是利用 dict 来实现的,因此它们有个共同的限制,即只 有 可散列 的数据类型才能用作这些映射里的键 ......
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而 且这个对象需要实现 __hash__() 方法。另外,可散列对象还要有 __eq__() 方法, 这样才能跟其它键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是 一样的 --- https://docs.python.org/3/glossary.html#term-hashable
原子不可变数据类型(str, bytes 和 数值类型)都是可散列类型, frozenset 也是 可散列的,因为根据其定义, frozenset 里只能容纳可散列类型。元组的话,只有当 一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的 ...... 一般来讲用 户自定义的类型的实例都是可散列的,散列值就是它们的 id() 函数的返回值,所以 所有这些对象在比较的时候都是不相等的。如果一个对象实现了 __eq__ 方法,并且 在方法中用到了这个对象的内部状态的话,那么只有当所有这些内部状态都是不可变的情 况下,这个对象才是可散列的。
....... dict.update 函数处理参数 m 的方式,是典型的“鸭子类型”。函数首先 检查 m 是否有 keys 方法,如果有,那么 dict.update 函数就把它当作映 射对象来处理。否则,函数会退一步,转而把 m 当作包含了键值对 (key, value) 元素的迭代器。Python 里大多数映射类的构造方法都采用了类似的逻辑。
当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉的 “快速失败” 哲学。也许每个 Python 程序员都知道可以用 d.get(k, default) 来代替 d[k] ,给找不到的链一个默认的返回值(这比处理 KeyError 要方便不少)。但是要更新某个键对应的值的时候,不管使用 __getitem__ 还是 get 都会不自然,而且效率低 ...... 可以用 dict.setdefault 解决这个问题。
有时候为了方便起见,就算某个键值在映射里不存在,我们也希望在通过这个键读取值 的时候能得到一个默认值。有两个途径能帮我们达到这个目的,一个是通过 defaultdict 这个类型,另一个是给自己定义一个 dict 的子类,然后在子类 中实现 __missing__ 方法 ...... 实际上, defaultdict 也是通过 __missing__ 方法实现的 ....... 虽然基类 dict 并没有定义这个方法,但是 dict 是知道有这么个东西存在的。也就是说,如果有个类继承了 dict ,然后这 个继承提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时 候, 它会调用该方法。
__missing__ 方法只会被 __getitem__ 调用(比如在表达式 d[k] 中)。提供 __missing__ 方法对 get 或者 __contains__ 这些方法的使用没有影响。 This method is called by the __getitem__() method of the dict class when the requested key is not found; whatever it returns or raises is then returned or raised by __getitem__().
就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类 要来得方便 ...... 后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的 子类中重写这些方法,但是 UserDict 就不会带来这些问题。另一个值得注意的地方 是, UserDict 并不是 dict 的子类,但是 UserDict 有一个叫作 data 的属性,是 dict 的实例,这个属性实际上是 UserDict 最终存储数据的地方 ...... UserDict 继承的是 MutableMapping 。
标准库里所有的映射类型都是可变的,但有时候你会有这样的需求,比如不能让用户错误 地修改某个映射 ...... Python 3.3 开始, types 模块中引入了一个封装类名叫 MappingProxyType 。它会为传递给它的映射创建一个只读映射视图。虽然是个只读视 图,但是它是动态的。这意味着如果对原映射做了改动,我们通过这个视图可以观察到。
集合
集合的本质是许多唯一对象的聚集。
集合 set 和 frozenset 在 Python 2.3 才首次以模块的形式出现,然后在 Python 2.6 中它们升级为内置类型。
集合中的元素必须是可散列的, set 类型本身是不可散列的,但是 frozenset 可以。因此可以创建一个包含不同 frozenset 的 set 。
除了保证唯一性,集合还实现了很多基础的中缀运算符: |, &, - ....... 可以使用诸如 {1}, {1, 2} 这样的集合字面量的形式创建非空集合,空集合和 frozenset 必须使用 set() 和 frozenset 的方式创建。
散列表
字典和集合底层使用散列表实现。散列表其实是一个稀疏数组。在一般的数据结构教材中 ,散列表里的单元通常叫作表元(bucket)。在 dict 的散列表当中,每个键值对都 占用一个表元,每个表元有两个部分,一个是对键的引用,另一个是对值的引用。因为所 有表元的大小一致,所以可以通过偏移量来读取某个表元。因为 Python 会设计保证大概 还有三分之一的表元是空的,所以在快要达到这个阈值的时候,原有的散列表会被复制到 一个更大的空间里面。
如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。Python 中可以用 hash() 函数来做这件事情 ...... 内置的 hash() 函数可以用于所有的内置类型 对象。如果是对自定义对象调用 hash() 的话,实际上运行的是自定义的 __hash__ 特殊方法。如果两个对象在比较的时候是相等的,那它们的散列值必须相等 ,否则散列表就一涌正常运行了 ...... 发生 散列冲突 时,散列算法会在散列值中另 外再取几位,然后用特殊方法处理一下(perturb),把新得到的数字再当作索引来寻找表 元。若这次找到的表元是空的,则同样抛出 KeyError ;若非空,或者键匹配,则返 回这个值;或者又发现了散列冲突,则重复以上的步骤 ...... 在插入新值时,Python 可 能会按照散列表的拥挤程度来决定是否要重新分配内存为它扩容。
散列表的特性给 dict 带来的优势和限制有:
键必须是可散列的。一个可散列的对象必须满足以下要求(所有由用户自定义的对象默 认都是可散列的,它们的散列值由 id() 来获取):
- 支持 hash() 函数,并用其散列值是不变的;
- 支持能过 __eq__() 方法检测相等性;
- 若 a == b 为真,则 hash(a) == hash(b) 也为真;
如果一个含有自定义的 __eq__ 的类处于可变的状态,那就不要在这个类中实现 __hash__ 方法,它的实例是不可散列的。
字典在内存上的开销巨大
键查询很快
键的次序取决于添加顺序,添加新键可能会改变己有键的顺序
上面提到的这些变化是否会发生以及如何发生,都依赖于字典背后的实现,因此你 不能很自信地说自己知道背后发生了什么。如果你在迭代一个字典的所有键的过程 中同时对字典进行修改,那么这个循环很有可能会跳过一些键 --- 甚至是跳过那些 字典中已经有的键。
set 和 frozenset 的实现也依赖散列表,但在它们的散列表里存放的只有元素的 引用。在 set 加入到 Python 之前,我都都是把字典加上无意义的值当作集合来用的 。上面提到的字典的特性的限制,对 set 和 frozenset 也都是适用的。
杂项
自 Python 2.7 以来,字典推导(dictcomp)可以从任何以键值对作为元素的可迭代 对象中构建出字段。
nd = {k: v for k, v in another_iterable}
自 Python 2.7 以来,集合推导(setcomp)可以用于创建集合。
ns = {k for k in another_iterable}
除了 dict 外,Python 标准库也提供了其它几种映射类型: defaultdict 、 OrderedDict 、 ChainMap 和 Counter ,以及用于用户扩展的 UserDict 类。
第 4 章 文本和字节序列
人类使用文本,计算机使用字节序列。 ———— Ester Nam 和 Travis Fischer, "Character Encoding and Unicode in Python"
Python 3 明确区分了人类可读的文本字符串和原始的字节序列。隐式地把字节序列转换成 Unicode 文本己成过去。
摘录
“字符串” 是个相当简单的概念:一个字符串是一个字符序列。问题出在 “字符” 的定义上 。在 2015 年,“字符” 的最佳定义是 Unicode 字符。因此,从 Python 3 的 str 对 象中获取的元素是 Unicode 字符,这相当于从 Python 2 的 unicode 的对象中获取 的元素,而不是从 Python 2 的 str 对象中获取的原始字节序列。
Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分。
- 字符的标识,即 码位 ,是 0~1114111 的数字,在 Unicode 标准中以 4~6 个十六 进制数字表示,而且加前缀 “U+”。在 Unicode 6.3 中(这是 Python 3.4 使用的标准 ),约 10% 的有效码位有对应的字符。
- 字符的具体表述取决于所用的 编码 。编码是在码位和字节序列之间转换时使用的 算法。在 UTF-8 编码中,A(U+0041)的码位编码成单个字节 x41,而在 UTF-16LE 编 码中编码成两个字节 x41x00。
把码位转换成字节序列的过程是 编码 ,把字节序列转换成码位的过程是 解码 ...... 可以把字节序列想成晦涩难懂的机器磁芯转储,把 Unicode 字符串想成 “人类可 读” 的文本。那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用 于存储或者传输的字节序列就是编码。
Python 自带了超过 100 种 编解码器 (codec,encoder/decoder),用于在文本和字 节之间相互转换。每个编解码器都有一个名称,如 'utf_8',而且经常有几个别名,如 'utf8','utf-8' 和 'U8'。
Python 3 默认使用 UTF-8 编码源码,Python 2(从 2.5 开始)则默认使用 ASCII ...... 可以在文件顶部添加一个神奇的 coding 注释。
如何找出字节序列的编码?简单来说,不能。必须有人告诉你 ...... 然后,就像人类语 言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找 出编码 ...... 统一字符编码侦测包 chardet 就是这样工作的,它能识别所支持的 30 种 编码。
处理文本的最佳实践是 “Unicode 三明治”。意思是,要尽早把输入的字节序列解码成字符 串。程序的业务逻辑只能处理字符串对象。在其他处理过程中,一定不能编码或解码。对 输出来说,则要尽量晚地把字符串编码成字节序列。
杂项
- -*- coding: utf8 -*- 指示 Python 如何解析源代码中的字符。
- sys.setdefaultencoding 设置 Python 的默认字符编码,该字符编码用于:
- decode 和 encode 函数的默认编码;
- base string 和 unicode string 进行比较、拼接等操作时,Python 会将 base string 使用此默认编码隐式转换成 unicode string 后,再进行操作;
- 在存储、传输和打印操作前,Python 会使用该编码将 Unicode string 进行转换;
第 5 章 一等函数
不管别人怎么说或怎么想,我从未觉得 Python 受到来自函数式语言的太多影响。我 非常熟悉命令式语言,如 C 和 Algol 68,虽然我把函数定为一等对象,但是我并不 把 Python 当作函数式编码语言。 ———— Guido van Rossum, Python 仁慈的独裁者
在 Python 中,函数是一等对象。编程语言理论家把 “一等对象” 定义为满足下述条件的 程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
摘录
接受函数为参数,或者把函数作为结果返回的函数是 高阶函数 (high-order function)。 map 函数就是一例,内置函数 sorted 也是 ...... 在函数式编程 范式中,最为人熟知的高阶函数有 map 、 filter 、 reduce 和 apply 。 apply 函数在 Python 3 中移除了,如果想使用不定量的参数调用函数,可以编写 fn(*args, **kwargs) ,不需再编写 apply(fn, args, kwargs) 了 ...... 在引 入了列表推导和生成器表达式后, map 和 filter 也变得没那么重要了。列表推 导或者生成器表达式具有 map 和 filter 两个函数的功能,而且更易于阅读 ...... reduce 函数最常用于求和,而自 2003 年发布的 Python 2.3 开始,求和最 好使用内置的 sum 函数。在可读性和和性能方面,这是一项重大改善。
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的 结果,把一系列值归约成一个值。 any 和 all 也是内置的归约函数。
lambda 关键字在 Python 表达式内创建匿名函数。然后,Python 简单的句法限制了 lambda 函数的定义体只能使用纯表达式 ...... 在参数列表中最适合使用匿名函数, 除此之外,Python 很少使用匿名函数。
除了用户定义的函数,调用运算法(即 () )还可以应用到其他对象上。如果想判断 对象能否调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了 7 种可调用对象:
用户定义的函数
内置函数。如 len()
内置方法。如 dict.get()
方法
类
调用类时会运行类的 __new__ 方法创建一个实例,然后运行实体的 __init__ 方 法,初始化实例,最后把实例返回给调用方。
类的实例。如定义了 __call__() 方法了类的实例
生成器函数
除了 __doc__ ,函数对象还有很多属性。使用 dir 函数可以获取这些属性列表 ...... 与用户定义的常规类一样,函数作为对象也使用 __dict__ 属性存储赋予它 的用户属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见 的做法, 但是 Django 框架这么做了。
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = "Customer name"
下面是函数专有而用户定义的一般对象没有的属性:
__annotations__ | dict | 参数和返回值的注解 |
__call__ | method-wrapper | 实现可设用对象协议 |
__closure__ | tuple | 函数闭包,即对自由变量的绑定 |
__code__ | code | 编译成字节码的函数元数据和函数 定义体 |
__defaults__ | tuple | 形式参数的默认值 |
__get__ | method-wrapper | 只读描述符协议方法 |
__globals__ | dict | 函数所在模块中的全局变量 |
__kwdefaults__ | dict | 仅限关键字形式参数的默认值 |
__name__ | str | 函数名称 |
__qualname__ | str | 函数的限定名称,如 Random.choice |
函数对象有个 __defaults__ 司性,它的值是一个元组,里面保存着定位参数和关键 字参数的默认值。仅限并键字参数的默认值在 __kwdefaults__ 属性中。然而,参数 的名称在 __code__ 属性中,该属性的值是一个 code 对象引用,它自身也有很 多属性 ...... 参数名称和函数定义体中创建的局部变量存储于 __code__.co_varnames 中,参数的个数存储于 __code__.co_argcount 中。顺便 说一下,这里不包含前缀为 * 和 ** 的变长参数。参数的默认值只能通过它们在 __defaults__ 元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起 来 ...... 我们可以使用 Python 标准库提供的内省模块 inspect 更方便的完成上述 操作。
Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据 ...... 注解不 会做任务处理,只是存储在函数的 __annotations__ 属性中。仅此而己,Python 不 做检查、不做强制、不做验证、什么操作都不做。换句话说,注解对 Python 解释器没有 任何意思。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。
虽然 Guido 明确表明,Python 的目标不是变成函数式编程语言,但是得益于 operator 和 functools 等包的支持,函数式编程风格也可以信手拈来 ...... operator 模块为多个算术运算符提供了对应的函数,从而避免编写 lambda 匿名 函数 ...... operator 模块还有一类函数,能替代从序列中取出元素或读取对象属性 的 lambda 匿名函数:因此, itemgetter 和 attrgetter 其实会自行构建 函数。 itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何 实现 __getitem__ 方法的类。如果把多个参数传给 itemgetter ,它构建的函数 会返回提取的值构成的元组。 attrgetter 与 itemgetter 类似,它创建的函数 根据名称提取对象的属性。如果把多个属性名传给 attrgetter ,它也会返回提取的 值构成的元组。
杂项
接受可迭代对象做为参数的函数,最好创建一个副本,防止迭代参数的意外副作用;
class Bingo(object): def __init__(self, items): self._items = list(items)
仅限关键字参数 Keyword-only argument
仅限关键字参数是 Python 3 新增的特性,它一定不会捕获未命名的定位参数。定 义 函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面。如果不 想支持 数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个 *,如 下所示: >>> def f(a, *args, b=None) pass >>> # or >>> def f(a, *, b): return a, b >>> f(1, b=2) (1, 2) 注意,仅限关键字参数不一定要有默认值,可以像上例中的 b 那样,强制必须传入 实参。
Python 2 对函数形式参数列表有如下规定:
- If a parameter has a default value, all following parameters must also have a default value --- this is a syntactic restriction that is not expressed by the grammar.
- The grammar shows *args and **kwargs should follow normal parameter with or without default values.
- Default parameter values are evaluated when the function definition is executed. This means that expression is evaluated once, when the function is defined, and that same "pre-computed" value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object, the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function.
第 6 章 使用一等函数实现设计模式
符合模式并不表示做得对。 ———— Ralph Johnson,经典的《设计模式:可复用面向对象软件的期础》的作者之一
虽然设计模式与语言无关,但这并不意味着每一个模式都能在每一门语言中使用。《设计 模式:可复用面向对象软件的基础》的作者在引言中承认,所用的语言决定了哪些模式可 用:
程序设计语言的选择非常重要,它将影响人们理解问题的出发点。我们的设计模式采 用了 Smalltalk 和 C++ 层的语言特性,这个选择实际上决定了哪些机制可以方便地 实现,而哪些则不能。若我们采用过程式语言,可能就要包括诸如 “集成” “封状” 和 “多态” 的设计模式。相应地,一些特殊的面向对像语言可以直接支持我们的某些模 式,例如 CLOS 支持多方法概念,这就减少了访问者模式的必要性。
具体而言,Norvig 建议在有一等函数的语言中重新审视 “策略” “命令” “模板方法” 和 “ 访问者” 模式。通常,我们可以把这些模式中涉及的某些类的实例替换成简单的函数,从 而减少样板代码。
在设计模式方面,Python 程序员的阅读选择没有其他语言多。
- 《Python Cookbook(第3版)》的 “8.21 实现访问者模式” 使用优雅的方式实现了 “访 问者” 模式。
- 《Learning Python Design Patterns》是唯一一本专门针对 Python 设计模式的书。不 过 Zlobin 这本书特别薄,只涵盖了 23 种设计模式中的 8 种。
- 《Python 高级编程》是市面上最好的 Python 中级书,第 14 章 “有用的设计模式” 从 Python 程序员的视角介绍了 7 种经典模式。
杂项
- globals() 函数返回一个字典,表示当前的全局符号表。这个符号表始终针对当前 模块。
- 人们经常引用 《设计模式:可复用面向对象软件的基础》这本书中的两个设计原则: “对接口编程,而不是对实现编程”,“优先使用对象组合,而不是类继承”。
第 7 章 函数装饰器和闭包
很多人抱怨,把这个特性命名为 “装饰器” 不好。主要原因是,这个名称与 GoF 书使 用的不一致,装饰器这个名称可能更适合在编译器领域使用,因为它会遍历并注解句 法树。 ———— "PEP 318: Decorators for Functions and Methods"
摘录
函数装饰器用于在源码中 “标识” 函数,以某种方式增强函数的行为。这是一项强大的功 能,但是若想掌握,必须理解闭包。
除了在装饰器中有用处之外,闭包还是回调式异步编程和函数式编程风格的基础。
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装 饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象 ...... 装饰器的一 个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在 导入时 ...... 而 被装饰的函数只在明确调用时运行。这突出了 Python 程序员所说的 导入时 和 运 行时 之间的区别。
多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返回,替换 被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运行。为了理解闭包,我们 先要了解 Python 中的变量作用域 ...... Python 不要求声明变量,但是假定在函数定义 体中赋值的变量是局部变量。
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全 局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。 函数的 __closure__ 属性保存着对这些自由变量的引用,变量名存储在函数的 __code__.co_freevars 中 ...... 综上,闭包是一种函数,它会保留定义函数时存在 的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑 定。注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
Python 3 引入了 nonlocal 声明,它的作用是把变量标记为自由变量,即使在函数中 为该变量赋予新值了,它也依然是自由变量 ...... Python 2 中没有 nonlocal ,因 此需要变量方法。其本上,可以将内部函数需要修改的非全局自由变量存储为可变对象的 元素或属性,并且把那个对象绑定给一个自由变量。
Python 内置了三个用于装饰方法的函数: propery, classmethod 和 staticmethod 。另一个常见的装饰器是 functools.wraps ,它的作用是协助构 建行为良好的装饰器。标准库中最值得关注的两个装饰器是 lru_cache 和全新的 singledispatch (Python 3.4 新增)。
杂项
- 自由变量(free variable)是指未在本地作用域绑定的变量。函数的 __code__ 属 性中保存局部变量( __code__.co_varnames )和自由变量( __code__.co_freevars )的名称。
第 8 章 对象引用、可变性和垃圾回收
“你不开心,” 白骑士用一种忧虑的声调说,“让我给你唱一首歌安慰你吧……这首歌的 曲名叫作:《黑线鳕的眼晴》。” “哦,那是一首哥的曲名,是吗?” 爱丽丝问道,她试着使自己感到有兴趣。 “不,你不明白,” 白骑士说,看来有些心烦的样子,“那是人家这么叫的曲名。真正 的曲名是《老而又老的老儿》。”(改编自第 8 章 “这是我自己的发明”) ———— Lewis Carroll,《爱丽丝镜中奇遇记》
摘录
变量不是盒子 。人们经常使用 “变量是盒子” 这样的比喻,但是这有碍于理解面向对 象语言中的引用式变量。Python 变量类似于 Java 中的引用式变量,因此最好把它们理解 为附加在对象上的标注 ...... 对引用式变量来说,说把变量分配给对象更合理,反过来 说就有问题。毕竟,对象在赋值之前就创建了。
为了理解 Python 的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之 后左边的变量才会绑定到对象上,这就像为对象贴上标注,忘掉盒子吧。
因为变量只不过是标注,所以无法阻止为对象贴上多个标注。贴的多个标注,就是 别名 。
Python 唯一支持的参数传递模式是 共享传参 ...... 共享传参指函数的各个形式参 数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。
我们应该避免使用可变的对象作为参数的默认值 ...... 默认值在定义函数时计算(通常 在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而 且修改了它的值,那么后续的函数调用都会受到影响。
如果定义的函数接收可变对象作为参数,应该谨慎考虑调用方是否期望修改传入的参数。 这其实需要函数的编写者和调用方达成共识。
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把 标识理解为对象在内存中的地址。 is 运算符比较两个对象的标识; id() 函数 返回对象标识的整数表示。
== 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象标识。然 而,在变量和 单例值 之间比较时,应该使用 is 。目前,最常用 is 检查变 量绑定的值是不是 None 。 is 运算符比 == 速度快,因为它不能重载,所 以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID。而 a == b 等同于 a.__eq__(b) 。继承自 object 的 __eq__ 方法比较两个对象的 ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 __eq__ 方法,会考虑对 象属性的值。
元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。而 str 、 bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是 引用,而是在连续的内存中保存数据本身(字符、字节和数字)。
如果元组引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的 不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对 象无关。
序列构造方法或者 [:] 运算符做得是 浅复制 (即复制了最外层容器,副本中的 源容器中元素的引用)。如果元素都是不可变的,那么这样没有问题,还能节省内存。但 是,如果有可变元素,可能就会导致意想不到的问题 ...... 有时候我们需要的是 深复制 (即副本不共享内部对象的引用)。 copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。
del 语句删除名称,而不是对象。 del 语句可能会导致对象被当作垃圾回收,但 是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时(循环引用)。重 新绑定也可能会导致对象的引用计数归零,导致对象被销毁 ...... del 语句不会直 接调用 __del__ 方法。而 __del__ 方法只在实例即将销毁前被解释器调用,它 给实例最后的机会,释放外部资源。自己编写的代码很少需要现 __del__ 代码。
在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少 引用指向自己,当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用 __del__ 方法(如果定义了的话),然后释放分配给对象的内存。CPython 2.0 增加 了分代垃圾回收算法,用于检测引用循环中涉及的对象组。
正是因为有引用,对象才会在内存中存在。当对象的引用计数归零后,垃圾回收程序会把 对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过它正常的生命周期。这 经常用在缓存中。
弱引用不会增加对象的引用计数,于是,它就不会妨碍所指对象被当作垃圾处理。Python 提供了 weakref 模块操作弱引用。
杂项
- 简单的赋值不创建副本。
- 对 += 或 *= 所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会 创建新对象;如果是可变对象,会就地修改。
- 为现在的变量赋予新值,不会修改之前绑定的变量。这叫重新绑定;现在变量绑定了其 他对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
- 函数的参数以别名的形式传递。
第 9 章 符合 Python 风格的对象
绝对不要使用两个前导下划线,这是很烦人的自私行为。 ———— Ian Bicking,pip、virtualenv、和 Paste 等项目的创建者
得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的 行为,靠的不是继承,而是 鸭子类型 :我们只需按照预定行为实现对象所需的方法 即可。
要构建符合 Python 风格的对象,就要观察真下的 Python 对象的行为。
正如你所知,我们要实现 __repr__ 和 __str__ 特殊方法,为 repr() 和 str() 提供支持。为了给对象提供其它的表示形式,还会用到另外两个特殊方法: __bytes__ 和 __format__ 。 __bytes__ 方法与 __str__ 方法类似: bytes() 函数调用它获取对象的字节序列表示形式。而 __format__ 方法会被内 置的 format() 函数和 str.format() 方法调用,使用特殊的可知式代码显示对 象的字符串表示形式。
格式说明符使用的表示法叫格式规范微语言(“Format Specification Mini-Language”, https://docs.python.org/3/library/string.html#formatspec)。
classmethod 装饰器非常有用,但是我从未见过不得不用 staticmethod 的情 况。如果想定义不需要与类交互的函数,那么在模块中定义就好了。有时,函数虽然从不 处理类,但是函数的功能与类紧密相关,因此想把它放在近处。即便如此,在同一模块中 的类前面或后面定义函数也就行了。
为了使类实例变成可散列的,必须使用 __hash__ 和 __eq__ 方法。此外,还要 让实例不可用 ...... 我们可以用 @property 装饰器把某个属性标记为只读。
要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确的 实现 __hash__ 和 __eq__ 方法即可。但是,实例的散列值绝不应该变化,因此我们 借机提到了只读特性(property)。
如果定义的类型有标量数值,可能还要实现 __int__ 和 __float__ 方法(分别 被 int() 和 float() 构造函数调用),以便在某些情况下用于强制转换类型。 此外,还有用于支持内置的 complex() 构造函数的 __complex__ 方法。
Python 不能像 Java 那样使用 private 修饰符创建私有属性,但是 Python 有个简 单的机制,能避免子类意外覆盖 “私有” 属性 ———— 使用双下划线开始的属性。Python 会 把这样的属性名存入类或实例的 __dict__ 中,而且会在前面加上一个下划线和类 名 ...... 这个语言特性叫 名称改写 (name mangling)。
名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意 做错事 ...... 不是所有 Python 程序员都喜欢名称改写功能,也不是所有人都喜欢 self.__x 这种不对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀 编写 “受保护” 的属性(如 self._x )。Python 解释器不会对使用单个下划线的属 性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问 这种属性。
不过在模块中,顶层名称使用一个前导下划线的话,的确会有影响:对 from mymod import * 来说,mymod 中前缀为下划线的名称不会被导入。然而,依旧可以使用 from mymod import _privatefunc 将其导入。
默认情况下,Python在各个实例中名为 __dict__ 的字典里存储实例属性。为了使用 底层的散列表提升访问速度,字段会消耗大量内存。如果要处理数百万个属性不多的实 例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属 性,而不用字典。定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例的 属性 ...... 在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实 例属性都在这儿了” 这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从 而避免使用消耗内存的 __dict__ 属性。如果有数百万实例同时活动,这样做能节省 大量内存。
在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其它 属性。这只是一个副作用,不是 __slots__ 存在的真正原因。不要使用 __slots__ 属性禁止类的用户新增实例属性。__slots__ 是用于优化的,不是为了约束程序员。
总之,如果使用得当, __slots__ 能显著节省内存,不过有几点要注意。
- 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属 性。
- 实例只能拥有 __slots__ 中列出的属性,除非把 __dict__ 加入 __slots__ 中(这样做就失去了节省内存的功效)。
- 如果不把 __weakref__ 加入 __slots__ ,实例就不能作为弱引用的目标。
- 使用了 __slots__ 并不能禁用向类中添加新的类属性。
Python 有个很独特的特性:类属性可用于为实例属性提供默认值。但是,如果为不存在的 实例属性赋值,会新建实例属性。自此以后,实例读的该属性就此实例属性了,也就是把 同名类属性遮盖了。借助这一特性,可以为各个实例的属性定制不同的值 ...... 如果想 要修改类属性的值,必须直接在类上修改,不能通过实例修改。
第 10 章 序列的修改、散列和切片
不要检查它是不是鸭子、它的叫声像不像鸭子、它的走路姿势像不像鸭子,等等。具 体检查什么取决于你想使用语言的哪些行为。 (comp.lang.python, 2000 年 7 月 26 日) ———— Alex Martelli
摘抄
在面向对象编程中, 协议 是非正式的接口,只在文档中定义,在代码中不定义。例 如,Python 的序列协议只需要 __len__ 和 __getitem__ 两个方法。任何类,只 要使用标准的签名和语义实现了这两个方法,就能用在任何期待序列的地方。是不是哪个 类的子类无关紧要,只要提供了所需的方法即可 ...... 我们说它是 序列 ,是因为它 的行为像序列,这才是重点 ...... 协议是非正式的,没有强制力,办此如果你知道类的 具体使用场景,通常只需要实现一个协议的部分。例如,为了支持迭代,只需实现 __getitem__ 方法,没必要提供 __len__ 方法。
在 Python 文档中,如果看到 “文件类对象” 这样的表述,通常说的就是协议。这是一种 简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求 的东西。”
你可能觉得只实现协议的一部分不够严谨,但是这样做的优点是简单。Python 语言参考手 册 “Data Model” 一章建议:
模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如, 有些序列可能只需要获取单个元素,而不必提取切片。
不要为了满足过度设计的接品契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则。
在使用切片语法访问序列实例时,它的 __getitem__ 方法接收到的是内置类型 slice 的实例。这个实例提供的 indices() 方法有如下作用:
S.indices(len) -> (start, stop, stride) 给定长度为 len 的序列,计算 S 表示的扩展切片的超始和结尾索引,以及步 幅。超出边界的索引会被截掉,这与常规切片的处理方式一样。
换句话说, indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引 负数索引,以及长度超过目标序列的切片。这个方法把 start, stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。
杂项
- 动态类型语言中的既定协议会自然进化。所谓的动态类型是指在运行时检查类型,因为 方法签名和变量没有静态类型信息。
- 如果实现了 __getattr__ 方法,那么也要定义 __setattr__ 方法,以防对象 的行为不一致。
- 在 Python 2 中使用 map 函数效率低些,因为 map 函数要使用结果构建一个 列表。但是在 Python 3 中, map 函数是惰性的,它会创建一个生成器,按需产出 结果,因此能节省内存。
- 只要有一次比较的结果是 False , all 函数函数就返回 False 。
- 使用 zip 函数能较松地并行迭代两个或更多可迭代对象,它返回的元组可拆包成变 量,分别对应各个并行输入中的一个元素。需要注意的是, zip 有一个奇怪的特 性:当一个可迭代对象耗尽后,它不发出警告就停止。而 itertools.zip_longest 的行为有所不同:使用可选的 fillvalue (默认值为 None )填充缺失的值, 因此可以继续产生,直到最长的可迭代对象耗尽。
第 11 章 接口:从协议到抽像基类
抽象类表示接口。 ———— Bjarne Stroustrup,C++ 之父
摘录
本章讨论的话题是接口:从鸭子类型的代表特征 动态协议 ,到使接口更明确、能验 证实现是否符合规定的 抽象基类 (Abstract Base Class,ABC)。
如果用过 Java,C# 或类似的语言,你会觉得鸭子类型的非正式协议很新奇。但是对长时 间使用 Python 或 Ruby 的程序员来说,这是接口的 “常规” 方式,新知识是抽象基类的 严格规定和类型检查。Python 语言诞生 15 年后,Python 2.6 才引入抽像基类。
引入抽象基类之前,Python 就已经非常成功了,即便现在也很少有代码使用抽象基类。我 们讨论了鸭子类型和协议,把协议定义为非正式的接口,是让 Python 这种动态类型语言 实现多态的方式。
duck-typing: A pythonic programming style which determines an object's type by inspection of its method or attribute signature rather than by explicit relationship to some type object ("if it looks like a duck and quacks like a duck, it must be a duck.") By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorhpic substitution. Duck-typing avoids tests using ``type()`` or ``isinstance()`` (Note, however, that duck-typing can be complemented with abstract base classes.) Instead, it typically employs ``hasattr()`` tests or EAFP programming.
关于接口,这里有个实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定的 角色。Python 文档中的 “文件类对象” 或 “可迭代对象” 就是这个意思,就种说指的不是 特定的类。接口是实现特定角色的方法集合。协议与继承没有关系。一个类可能会实现多 个接口,从而让实例扮演多个角色。协议是接口,但不是正式的(只由文档和约定定 义),因此协议不能像正式接口那样施加限制。一个类可能只实现部分接口,这是允许 的。有时,某些 API 只要求 “文件类对象” 返回字节序列的 .read() 方法。在特定 的上下文中可能需要其他文件操作方法,也可能不需要 ...... 对 Python 程序员来说, “X 类对象”、“X 协议” 和 “X 接口” 都是一个意思。
鸭子类型技术忽略对象的真正类型,转而关注对象有没有实现所需的方法、签名和语义。 对 Python 来说,这基本上是指避免使用 isinstance 检查对象的类型 ...... 总的 来说,鸭子类型技术在很多情况下十分有用;但是在其他情况下,随着发展,通常有更好 的方式 ...... 支序系统学 ...... DNA ...... 但是其它方面,如对不同病原体的抗性, DNA 接近性的作用就大多了 ...... 在鸭子类型的基础上增加白鹅类型(goose-typing) ...... 白鹅类型指,只要 cls 是抽象基类,即 cls 的元类是 abc.ABCMeta ,就可以使用 isinstance(obj, cls) ...... 与具体类相比,抽象 基类有很多理论上的优点,Python 的抽象基类还有一个重要实用优势:可以使用 register 类方法在终端用户的代码中把某个类 “声明” 为一个抽象基类的 “虚拟” 子 类(为此,被注册的类必腨满足抽象其类对方法名称和签名的要求,最重要的是要满足底 层语义契约;但是,开发那个类时不用了解抽象基类,更不用继承抽象基类 ....... 有 时,为了让抽象类识别子类,甚至不用注册 ...... 要抑制住创建抽象基类的冲动。滥用 抽象基类会造成灾难性后果,表明语言太注重表面形式,这对以实用和务实著称的 Python 可不是好事儿。
继承抽象类的子类定义时,Python 并不会检查其对抽象方法的实现,在运行时对该子类实 例化时才会真正检查。如果没有正确实现某个抽象方法,Python 会抛出 TypeError 异常 ...... 在 collection.abc 中,每个抽象基类的具体方法都是作为类的公开接 口实现的 .... 在实现子类时,我们可以覆盖从抽象类中继承的方法,以更高效的方式重 新实现。例如, __contains__ 方法会全面扫描序列,可是,如果你定义的序列按顺 序保存元素,那就可以重新定义 __contains__ 方法,使用 bisect 函数做二分 查找,从而提升搜索速度。
从 Python 2.6 开始,标准库提供了抽象基类。大多数抽象基类在 collections.abc 模块中定义,不过其它地方也有。例如, numbers 和 io 包中有一些抽象基类。
白鹅类型的一个基本特性:即便不继承,也有办法把一个类注册为抽象基类的 虚拟子类 。这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而 Python 会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕 获 ...... 注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后, 注册的类会变成抽象基类的虚拟子类,而用 issubclass 和 isinstance 等函数 都能识别,但是注册的类不会从抽象基类中继承任何方法或属性 ...... 虚似子类不会继 承注册的抽象基类,而且任何时候都不会检查它是否符俣抽象基类的接口,即便在实例化 时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法。
Python 3.3 前,不能将 register 方法当作装饰器使用。虽然现在可以把 register 当作装饰器使用了,但更常见的做法还是把它当作函数使用,用于注册其他 地方定义的类。例如,在 collections.abc 模块的源码中,是这样把内置类型 tuple 、 str 、 range 和 memoryview 注册为 Sequence 的虚拟 子类的:
Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
即便不注册,抽象基类也能把一个类识别为虚拟子类。比如下面定义的类 Struggle
>>> class Struggle(object):
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
Struggle 也成了 Sized 的子类。这是因为 abc.Sized 实现了一个特殊的类 方法,名为 __subclasshook__ 。下面是 Sized 的 __subclasshook__ 实现 代码:
class Sized(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
如果 C 或其任一超类实现了 __len__ 方法时,都会被 Sized 当成虚拟子类。 __subclasshook__ 在白鹅类型中添加了一些鸭子类型的踪迹 ...... 我们可以使用 抽象基类定义正式接口,也可以完全使用不相关的类,只要实现特定的方法即可(或者做 些事情让 __subclasshook__ 信服)。当然,只有提供 __subclasshook__ 方法 的抽象基类才能这么做。
最后的警告:不要自己定义抽象基类,除非你要构建允许用户扩展的框架————然而大多数 情况并非如此。日常使用中,我们与抽象基类的联系应该是创建现有抽象基类的子类,或 者使用现有的抽象基类注册虚拟子类。此外,我们可能还会在 isinstance 检查中使 用抽象基类,但这比继承或注册更少见。需要自己从头编写新抽象基类的情况少之又少。
尽管抽象基类使得类型检查变得更容易了,但不应该在程序中过度使用它。Python 的 核心在于它是一门动态语言,它带来了极大的灵活性。如果处理都强制实行类型约束, 那么会使代码变得更加复杂,而本不应该如此。我们应该拥抱 Python 的灵活性。 ———— David Beazley 和 Brian Jones 《Python Cookbook(第3版)》
杂项
序列协议: __getitem__, __len__ 。实现了这两个方法的类,虽然未继承 abc.Sequence ,但可以支持迭代和 in 运算符。如果需要实现可变序列协议, 还必须提供 __setitem__ 方法。
如果遵守了既定协议,很有可能增加利用现有的标准库和第三方代码的可能性,这得益 于鸭子类型。
若想检查对象是否可调用,可以使用内置的 callable() 函数;但是 Python 并未 提供检查对象是否可散列的函数,此时可以使用 isinstance(obj, Hashable) 的方 式。
在抽象基类出现之前,一般使用 raise NotImplementedError 实现抽象方法。
定义自己的抽象基类时要注意的事项:
- 抽象基类需要继承 abc.ABC (Python 3+)或者设置元类为 abc.ABCMeta 。
- 抽象方法使用 @abstractmethod 装饰器标记,而且定义体中通常只有文档字符 串。
- 抽象基类可以包含具体方法,但是具体方法只能依赖抽象基类定义的接口(即只能使 用抽象基类中的其他具体方法、抽象方法或特性)。
- 抽象方法可以有实现代码。但是子类也必须覆盖该抽象方法,此时在子类实现中可以 使用 super() 函数调用父类抽象方法。
抽象基类不会出现在使用 register 注册的虚拟子类的 __mro__ 属性中。同 时,抽象的直接子类列表( __subclasses__() 函数的返回值)也不包含虚拟子 类。抽像基类有一个类型为 WeakSet 的数据属性 _abc_registry ,其中存放 着抽象基类注册的虚拟子类的弱引用。
__subclasshook__ 方法在手册中的说明如下:
__subclasshook__(subclass) Check whether *subclass* is considered a subclass of this ABC. This means that you can customize the behavior of issubclass further without the need to call register() on every class you want to consider a subclass of the ABC. (This class method is called from the __subclasscheck__() method of the ABC.) This method should return True, False, or NotImplemented. If it returns True, the *subclass* is considered a subclass of this ABC. If it returns False, the *subclass* is not considered a subclass of this ABC, even if it would normally be one. If it returns NotImplemented, the subclass check is continued with the usual mechanism.
第 12 章 继承的优缺点
(我们)推出继承的初衷是让新手顺利使用只有专家才能设计出来的框架。 ———— Alan Key,"The Early History of Smalltalk"
摘录
很多人觉得多重继承得不偿失。不支持多重继承的 Java 显然没有什么损失,C++ 对多重 继承的滥用伤害了很多人,这可能还坚定了使用 Java 的决心。
子类化内置类型很麻烦。在 Python 2.2 之后,内置类型可以子类化了,但是有个重要的 注意事项:内置类型(使用 C 语言编写)不会调用用户定义的特殊方法。PyPy 的文档使 用简明扼要的语言描述了这个问题,见于 “Differences between PyPy and CPython” 中 “Subclasses of built-in types” 一节:
Officially, CPython has no rule at all for when exactly overridden method of subclasses of built-in types get implicitly called or not. As an approximation, these methods are never called by other built-in methods of the same object. For example, an overridden ``__getitem__()`` in a subclass of ``dict`` will not be called by e.g. the built-in ``get()`` method.
(FIXME: get() 本来也不会调用 __getitem__() 啊,上面讲 __missing__ 用法的时候不是明确说明过么?)
可以通过子类化 UserDict 来解决上述的子类化 dict 带来的问题。
上面所述的问题只发生在 C 语言实现的内置类型内部的方法委托上,而且只影响直接继参 内置类型的用户自定义类。如果子类化使用 Python 编写的类,如 UserDict 或者 MutableMapping ,就不会受引影响。
任何实现多重继承的语言都要处理潜在的命名冲突,就种冲突由不相关的祖先类实现同名 方法引起。这种冲突问题称为 “菱形问题” ...... Python 会按照特定的顺序遍历继承 图。这个顺序叫方法解析顺序(Method Resolution Order,MRO)。类都有一个名为 __mro__ 的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一 直向上,直到 object 类。 super() 函数也会使用方法解析顺序查找属性 ...... 子类声明中列出的超类顺序会影响方法解析顺序。
方法解析顺序使用 C3 算法计算。Michele Simionato 的论文 “The Python 2.3 Method Resolution Order” 对 Python 方法解析顺序使用的 C3 算法做了权威论述。
多重继承能发挥积极作用。《设计模式:可复用面向对象软件的基础》一书中的适配器模 式用的就是多重继承,因此使用多重继承肯定没有错(那本书中的其他 22 个设计模式都 使用单继承,因此多重继承显然汪是灵丹妙药)。
继承有很多用途,而多重继承增加了可选方案和复杂度。便用多重继承容易得出令人费解 和脆弱的设计。我们还没有完整的理论,下面是避免把类图搅乱的一些建议。
把接口继承和实现继承分开
使用多重继承时,一定要明确一开始为什么什么建子类。主要原因可能有: * 继承接口,创建之类型,实现 “是什么” 关系 * 继承实现,通过重用避名代码重复 其实这两条经常同时出现,不过只要可能,一定要明确意图。通过继承重用代码是 实现细节,通常可以换用组合和委托模式。而接口继承是框架的支柱。
使用抽象基类显式表示接口
现代 Python 中,如果类的作用是定义接口,就该明确把它定义为抽象基类。
通过混入(Mixin)重用代码
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现 “是什么” 关系,应该把那个类明确定义为 混入类(mixin class)。从概念上 讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且 具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非 常紧密的方法。
在名称中明确指明混入
因为在 Python 中没有把类声明为混入的正规方式,所以强烈推荐在名称中加入 ``Mixin`` 后缀。
抽象基类可以作为混入,反过来则不成立
抽象基类可以实现具体方法,因此也可以作为混入使用。不过,抽象基类会定义类 型,而混入做不到。此外,抽象基类可以作为其他类的唯一基类,而混入决不能作 为唯一的超类,除非继承另一个更具体的混入————真实代码很少这么做。 抽象基类有个局限是混入没有的:抽象基类中实现的具体方法只能与抽象基类及其 超类中的方法协作。这表明,抽象基类中的具体方法只是一种便利措施,因为这些 方法所做的一切,用户调用抽象基类中的其他方法也能做到。
不要子类化多个具体类
具体类可以没有,或最多只有一个具体超类。也就是说,具体类的超类中除了这一 个具体超类之外,其余的都是抽象基类或混入。
为用户提供聚合类
如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解 的方式把它们结合起来。Grady Booch 把这种类称为聚合类。
优先使用对象组合,而不是类继承
杂项
第 13 章 正确重载运算符
有些事情让我不安,比如运算符重载。我决定不支持运算符重载,这完全是个人选 择,因为我见过太多的 C++ 程序员滥用它。 ———— James Gosling,Java 之父
摘录
运算符重载的作用是让用户定义的对象使用中缀运算符或一元运算符。说得宽泛一些,在 Python 中,函数调用、属性访问和元素访问/切片 也是运算符,不过本章只讨论一元运算 符和中缀运算符。
在某些圈子里,运算符重载的名声并不好。这个语言特性可能已经被滥用,让程序员困 惑,导致缺陷和意料之外的性能瓶颈。但是,如果使用得当,API 会变得好用,代码会变 得易于阅读。Python 施加了一些限制,做好了灵活性、可用性和安全性方面的平衡:
- 不能重载内置类型的运算符
- 不能新建运算符,只能重载现有的
- 某些运算符不能重载———— is 、 and 、 or 和 not
Python 有三个一元运算符: +, -, ~ 。它们对应的特殊方法分别为: __pos__, __neg__ 和 __invert__ ...... Python 语言参考手册中的 “Data Model” 一章还把内置的 abs() 函数列为一元运算符。它对应的特列方法是 __abs__ 。
支持一元运算符很简单,只需实现相应的特殊方法。这些特殊方法只有一个参数, self 。然后,使用符合所在类的逻辑实现。不过,要遵守运算符的一个基本规则:始 终返回一个新对象。也就是说,不能修改 self ,要创建并返回合适类型的新实例。
同一元运算符一样,中缀运算符的方法一定不能修改操作数。使用这些运算符表达式期待 结果是新对象。只有增量赋值表达式可能会修改第一个操作数( self )。
为了支持不同类型的运算,Python 为中缀运算符特殊方法提供了特殊的分派机制。对表达 多 a + b 来说,解释器会执行以下几步操作:
- 如果 a 有 __add__ 方法,而且返回值不是 NotImplemented ,调用 a.__add__(b) ,然后返回结果。
- 如果 a 没有 __add__ 方法,或者调用 __add__ 方法返回 NotImplemented ,检查 b 有没有 __radd__ 方法,如果有,而且没有返 回 NotImplemented ,调用 b.__radd__(a) ,然后返回结果。
- 如果 b 没有 __radd__ 方法,或者调用 __radd__ 方法返回 NotImplemented ,抛出 TypeError ,并在错误消息中指明操作数类型不支持。
如果由于类型不兼容而导致运算符特殊方法无法返回有效的结果,那么应该返回 NotImplemented ,而不是抛出 TypeError 。返回 NotImplemented 时,另 一个操作数所属的类型还有机会执行运算,即 Python 会尝试调用反向方法。对调之后, 反向运算符方法可能会正确计算。
Python 解释器对众多比较运算符( ==, !=, >, <, >=, <= ) 的处理与前文类似,不过在两个方面有重大区别:
- 正向和反向调用使用的是同一系列方法。这方面的规则如下表。例如,对 == 来 说,正向和反向调用都是 __eq__ 方法,只是把参数对调了;而正向的 __gt__ 方法调用的是返回的 __lt__ 方法,并把参数对调。
- 对 == 和 != 来说,如果反向调用失败,Python 会比较对象的 ID,而不抛出 TypeError 。
中缀表达式 | 正向方法调用 | 反向方法调用 | 后备机制 |
---|---|---|---|
a == b | a.__eq__(b) | b.__eq__(a) | 返回 id(a) == id(b) |
a != b | a.__ne__(b) | b.__ne__(a) | 返回 not (a == b) |
a > b | a.__gt__(b) | b.__lt__(a) | 抛出 TypeError |
a < b | a.__lt__(b) | b.__gt__(a) | 抛出 TypeError |
a >= b | a.__ge__(b) | b.__le__(a) | 抛出 TypeError |
a <= b | a.__le__(b) | b.__ge__(a) | 抛出 TypeError |
如果一个类没有实现就地运算符,增量赋值运算符只是语法糖: a += b 的作用与 a = a + b 完全一样。对不可变类型,这是预期的行为,而且,如果定义了 __add__ 方法的话,不用编写额外的代码, += 就能使用。然而,如果实现了就 地运算符方法,例如 __iadd__ ,计算 a += b 的结果时会调用就地运算符方法 。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。
杂项
第 14 章 可迭代的对象、迭代器和生成器
当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式 应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号。(到少 对我来说)表明我对问题的抽象还不够深————这通常意味着自己正在手动完成的事情 ,本应该通过写代码来让宏的扩展自动实现。 ———— Paul Graham,Lisp 黑客和风险投资人
摘录
迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项 的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator Pattern)。与 Lisp 不同,Python 没有宏,因此为了抽象出迭代器模式,需要改动语言本身。为此,Python 2.2 加入了 yield 关键字。这个关键字用于构建生成器,其作用与迭代器一样。
所有生成器都是迭代器,因为生成器完全实现了迭代器接口。在 Python 3 中,生成器有 广泛的用途。现在,即使是内置的 range() 函数也返回一个类似生成器的对象,而以 前则返回完整的列表。
解释器需要迭代对象 x 时,会自动调用 iter(x) 。内置的 iter 函数有以 下作用:
- 检查对象是否实现了 __iter__ 方法,如果实现了就调用它,获取一个迭代器。
- 如果没有实现 __iter__ 方法,但是实现了 __getitem__ 方法,Python 会创 建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。
- 如果尝试失败,Python 抛出 TypeError 异常,通常会提示 “C object is not iterable”,其中 C 是目标对象所属的类。
从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法是:调用 iter(x) 函数,如果不可迭代,再处理 TypeError 异常。这比使用 isinstance(x, abc.Iterable 更准确,因为 iter(x) 函数会考虑到遗留的 __getitem__ 方 法,而 abc.Iterable 类则不考虑。
使用 iter 函数可以获取迭代器的对象。如果对象实现了能返回迭代器的 __iter__ 方法,那么对象就是可迭代的 ...... Python 从 可迭代对象 中获取 迭代器 ...... 使用 iter() 函数获取迭代器,使用 next() 函数使用迭代 器。
标准的迭代器接口有两个方法。 __next__ 方法返回下一个可用的元素,如果没有元 素了,抛出 StopIteration 异常; __iter__ 方法返回 self ,以便在应该 使用可迭代对象的地方使用迭代器。由于 abc.Iterator 实现的 __subclasshook__ 方法也检查了这两个方法,所以可以使用 isinstance(x, abc.Iterator) 的方式判断对象是否是迭代器,不管该对象所属的类是否是 abc.Iterator 的真实子类或虚拟子类。
因为迭代器只需 __next__ 和 __iter__ 两个方法,所以除了调用 next() 函数,以及捕获 StopIteration 异常外,没有办法检查是否还有遗留的元素。此外, 也没有办法 “还原” 迭代器。如果想再次迭代,那就要调用 iter() 函数再次创建新 的迭代器。传入迭代器本身没有用,因为前面说过 Iterator.__iter__ 方法的实现方 式是返回迭代器本身,所以传入迭器无法还原已经耗尽的迭代器。
只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器 函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂 ...... 调用生成 器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给 next() 函 数时,生成器函数会向前执行函数定义体中的下一个 yield 语句,返回产出的值,并 在函数定义体的当前位置暂停。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常————这一点与迭代器协议一致。
生成器不会以常规的方式 “返回” 值:生成器函数定义体中的 return 语句会触发生 成器对象抛出 StopIteration 异常。在 Python 3.3 之前,如果生成器函数的 return 语句有返回值,那么会报错。在 Python 3.3 之后的版本里,return 语句可 以携带返回值,但是它仍会触发 StopIteration 异常。但是,此时调用方可以从异常 对象中获取该返回值。
生成器表达式可以理解为列表推导的惰性版本:它不会迫切地构建列表,而是返回一个生 成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达 式就是制造生成器的工厂 ...... 生成器表达式是生成器函数的语法糖 ...... 生成器表 达式是创建生成器的简洁句法,这样无需先定义函数再调用。不过,生成器函数灵活得 多,可以使用多个语句实现强大的逻辑,也可以作为 协程 使用 ...... 选择使用哪 种句法很容易判断:如果生成器表达式要分成多行写,就可以使用生成器函数了,以便提 高可读性。此外,生成器函数有名称,可以重复使用。
Python 3.4 中的 itertools 模块提供了 19 个生成器函数。其它标准库也提供了很 多其它功能的生成器函数。很多生成器函数可以组合在一起使用 ...... 如果生成器函数 需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的 for 循环。比如,下 面是我们自己实现的 chain 生成器函数:
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = "ABC"
>>> t = tuple(range(3))
>>> list(chain(s, t))
["A", "B", "C", 0, 1, 2]
chain 生成器函数把操作依次交给接收到的各个可迭代对象处理。为此,“PEP 380 - Syntax for Delegating to a Subgenerator” 引入了一个新句法:
>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
["A", "B", "C", 0, 1, 2]
yield from i 完全代替了内层的 for 循环。除了代替循环外, yield from 还会创建通道,把内层生成器真接与外层生成器的客户端联系起来。把生成器当成协程使 用时,这个通道特别重要,不权能为客户端代码生成值,还能使用客户端代码提供的值。
Python 2.2 引入了 yield 关键字实现的生成器函数,大约五年后,Python 2.5 实现 了 “PEP 342 - Coroutines via Enhanced Generators”。这个提案为生成器对象添加了额 外的方法和功能,其中最值得关注的是 .send() 方法 ...... 与 .__next__() 方法一样, .send() 方法使生成器前进到下一个 yield 语句。不过, .send() 方法还允许使用生成器的客户把数据发给自己,即不管传给 .send() 方 法什么参数,那个参数都会成为生成器函数定义体中对应的 yield 表达式的值。也就 是说, .send() 方法允许在客户代码和生成器之间双向交换数据。而 .__next__() 方法只允许客户从生成器中获取数据。这是一项重要的改进,甚至改变 了生成器的本性:像这样使用的话,生成器就变身为 协程 。
在 PyCon US 2009 期间举办的一直播著名的课程中( http://www.dabeaz.com/coroutines/ ),David Beazley(可有是 Python 社区中在协程 方面最多产的作者和演讲者)提醒道:
- 生成器用于生成供迭代的数据
- 协程是数据的消费者
- 为了避免脑袋炸裂,不能把这两个概念混为一谈
- 协程与迭代无关
- 注意,虽然在协程中会使用 yield 产出值,但这与迭代无关
杂项
如果函数或构造方法只有一个参数,传入生成器表达式时不用写一对调用函数的括号, 再写一对括号围住生成器表达式,只写一对括号就行了。
sum(n for n in range(5))
第 15 章 上下文管理器和 else 块
最终,上下文管理器可能几乎与子程序(subroutine)本身一样重要。目前,我们只 了解了上下文管理器的皮毛......Basic 语言有 with 语句,而且很语言都有。但 是,在各种语言中 with 语句的作用不同,而且做的都是简单的事,虽然可以避免不 断使用点号查找属性,但是不会做事前准备和事后清理。不要觉得名字一样,就意味 着作用也一样。with 语句是非常了不起的特性。 ———— Raymond Hettinger,雄辩的 Python 布道者
摘录
这个语言特性不是什么秘密,但却没有得到重视: else 子句不仅能在 if 语句 中使用,还能在 for 、 while 和 try 语句中使用。 for/else 、 while/else 和 try/else 的语义关系紧密,不过与 if/else 差别很大:
- for/else - 仅当 for 循环运行完毕时(迭代数据用尽,没有被 break 语句提前中断)才运行 else 块代码。
- while/else - 仅当 while 循环运行完毕时(循环条件为假,没有被 break 语句提前中断)才运行 else 块代码。
- try/else - 仅当 try 块中没有异常抛出时才运行 else 块代码。
在所有情况下,如果异常或者 return 、 break 或 continue 语句导致控制 权跳到了复合语句的主块之外, else 子句也会被跳过。
上下文管理器对象存在的目的是为了用于 with 语句,就像迭代器的存在是为了 for 语句一样。 with 语句用于简化 try/finally 模式,它用于保证一段代 码运行完毕后执行某项操作,即便那段代码由于异常、 return 语句或 sys.exit() 调用而中止,也会执行指定的操作(比如释放重要资源,或者还原临时变 更的状态等)。
上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。 with 语句开始 运行时,会调用上下文管理器对象的 __enter__ 方法。 with 语句运行结束后, 会调用其 __exit__ 方法,使其扮演 finally 子句的角色 ...... __enter__ 方法的返回值可以被 as 子句绑定到一个变量上 ...... 如果 with 块中抛出了异常, __exit__ 方法会从参数得到相关异常的信息。如果 __exit__ 方法处理了异常,并且不需要 with 表达式将此异常继续向上抛出的 话, __exit__ 需要返回 True ,随后 with 表达式就会压制该异常。如果 __exit__ 方法没有显式返回一个值,那么 with 得到 None 值,会将此异常 向上抛出。
with LookingGlass() as what:
print(what)
Python 标准库提供了 contextlib 模块,它可以辅助用户定义和使用上下文管理器。 下面是 contextlib.contextmanager 的一个使用示例:
import contextlib
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
yield "JABBERWOCKY"
sys.stdout.write = original_write
with looking_glass() as what:
print(what)
@contextmanager 装饰器优雅且实用,把三个不同的 Python 特性结合到一起:函数 装饰器、生成器、和 with 语句。
杂项
- EAFP vs. LBYL
第 16 章 协程
如果 Python 书籍有一定的指导作用,那么(协程就是)文档最匮乏、最鲜为人知的 Python 特性,因此表面上看是最无用的特性。 ———— David Beazley,Python 图书作者
摘录
字段为动词 “to yield” 给出了两个释义:产生和让步。对于 Python 生成器中的 yield 来说,这两个含义都成立。 yield item 这行代码会产出一个值,提供给 next() 的调用方;此外,还会作出让步,暂停执行生成器,让周用方继续工作,直到 需要使用另一个值时再调用 next() 。调用方会从生成器是拉取值。
从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。可是,在 协程中, yield 通常出现在表达式的右边,可以产出值,也可以不产出————如果 yield 关键字后面没有表达式,那么生成器产出 None 。协程可能会从调用方接 收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next() 函数。通常,调用方会把值推送给协程。
yield 关键字甚至还可以不接收或传出数据。不管数据如何流动, yield 都是一 种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程度 ,从而激活其它协程。
从根本上把 yield 视作控制流程的方式,这样就好理解协程了 。
协程的底层架构在 “PEP 342 - Coroutines via Enhanced Generators” 中定义,并在 Python 2.5(2006 年)实现了。自此之后, yield 关键字可以在表达式中使用,而 且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的 值。因此,生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出 由调用方提供的值。
除了 .send() 方法,PEP 342 还添加了 .throw() 和 .close() 方法:前者 的作用是让调用方抛出异常,在生成器中处理;后者的作用是终止生成器。
协程最近的演进来自 Python 3.3(2012年)实现的 “PEP 380 - Syntax for Delegating to a Subgenerator”。 PEP 380 对生成器函数的句法做了两处改动,以便更好地作为协程 使用:
- 现在,生成器可以 return 一个值;以前,如果在生成器中给 return 语句提 供值,会抛出 SyntaxError 异常。
- 新引入了 yield from 句法,使用它可以把复杂的生成器重构成小型的嵌套生成 器,省去了之前把生成器的工作委托给子生成器所需的大理样板代码。
协程可以身处四个状态中的一个。当前状态可以使用 inspect.getgeneratorstate() 函数获取,该函数会返回下述字符串中的一个:
- GEN_CREATED - 等待开始
- GEN_RUNNING - 正在执行(在多线程应用中才能看到这个状态,或者生成器对象在 自己身上调用 getgeneratorstate 函数也能看到这个状态,不过没有什么意义)
- GEN_SUSPENDED - 在 yield 表达式处暂停
- GEN_CLOSED - 执行结束
刚刚创建的协程处于 GEN_CREATED 状态中,调用者需要使用 next(my_coro) 或 者 my_coro.send(None) 启动/激活这个协程,被激活的协程开始运行,直到第一个 yield 表达式,之后这个协程就准备好作用活跃的协程使用了(这一步通常称为 “预激” 协程)。如果使用 .send() 传递 None 以外的值给刚刚创建的协程, 会出现下述错误:
>>> my_coro = simple_coroutine()
>>> my_coro.send(1729)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
很多框架都提供了处理协程的特殊装饰器,不过不是所有装饰器都用于预激协程,有些会 提供其他服务,例如勾入事件循环 ...... 使用 yield from 句法调用协程时,会自 动预激协程。Python 3.4 标准库里的 asyncio.coroutine 装饰器不会预激协程,因 此能兼容 yield from 句法。
协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触 发协程的对象)...... 从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方 法,显式地把异常发给协程,这两个方法是 throw 和 close 。 throw 方法 致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了该异常,代 码会向前执行到下一个 yield 表达式,而产出的值会成为 throw 方法的返回 值;如果生成器没有处理该异常,异常会向上冒泡,传到调用方的上下文中 ...... close 方法致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异 常,如果该协程没有处理这个异常,或者抛出了 StopIteration 异常,调用方不会报 错。收到 GeneratorExit 异常后,协程不能再产出值,否则解释器会抛出 RuntimeError 异常。但是此时协程可以抛出其它异常,这些异常会传冒泡到调用方。
另外,还可以通过向协程传递 “哨兵值”,协程收到该 “哨兵值” 后退出函数。内置的 None 和 Ellipsis 等常量经常用作哨兵值。 Ellipsis 的优点是,数据流中 不太常有这个值。
yield from 是全新的语言结构,它的作用比 yield 多很多 ...... 它的主要功 能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接 发送和产出值,还可以直接传入异常,而不用在位于中间的协程添加大量处理异常的样板 代码。
若想使用 yield from 结构,就要大幅改动代码,为了说明需要改动的部分,PEP 380 使用了一些专门的术语:
- 委派生成器 - 包含 yield from <iterable> 表达式的生成器函数。
- 子生成器 - 从 <iterable> 部分获取的生成器。
- 调用方 - 调用委派生成器的客户端代码。
委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成 器,子生成器再把产出的值发给调用方。子生成器返回后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复执行。
yield from 表达式支持的 <iterable> 部分可以是简单的只实现了 __next__ 方法的迭代器,也可以是实现了 __next__ 、 send 、 close 和 throw 方法的生成器。
PEP 380 中对 yield from 的行为做了如下说明:
- 子生成器产出的值都直接传给委派生成器的调用方。
- 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None ,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None ,那么会调用子生成器的 send() 方法。如果子生成器抛出 StopIteration 异常,那委派生成器恢复运行。任何其它异常都会向上冒泡,传给 委派生成器。
- 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
- yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个 参数值。
- 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出了 StopIteration 异常,委派生成器恢 复运行。 StopIteration 之外的异常会向上冒泡,传给委派生成器。
- 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么(委派生成器)调用子生成器的 close() 方法(如果子生 成器提供了该方法的话)。如果子生成器的 close() 方法导致异常抛出,那么异常 会向上冒泡,传给委派生成器。如果子生成器的 close() 方法正常执行的话,委派 生成器随后向上抛出 GeneratorExit 异常。
杂项
- 协程在其代码执行完毕、协程对象引用计数变为 0 ,或者调用方在协程上调用了 .close() 方法后,这个协程才会终止。
- 从 Python 3.3 开始,生成器函数可以使用 return 表达式返回值给调用者。但 是, return 表达式的值会偷偷传给调用方,赋值给 StopIteration 异常的一 个属性。这样做有点不合常理,但是能保留生成器对象的常规行为————耗尽时抛出 StopIteration 异常。
- 使用 yield from 表达式时,子生成器使用 return 表达式返回的值,会作为 yield from 表达式的返回值,返回给委派生成器。
第 17 章 使用期物处理并发
抨击线程的往往是系统程序员,他们考虑的使用场景对一般的应用程序员来说,也许 一生都不会遇到……应用程序员遇到的使用场景,99% 的情况下只需知道如何派生一堆 独立的线程,然后用队列收集结果。 ———— Michele Simionato,深度思考 Python 的人
摘录
本章主要讨论 Python 3.2 引入的 concurrent.futures 模块,从 PyPI 中安装 futures 包之后,也能在 Python 2.5 及以上版本使用这个库。这个库封装了前面的 引文中 Michele Simionato 所述的模式,特别易于使用。
我们用期物(future)表示异步执行的操作。这个概念的的作用很大,是 concurrent.futures 模块和 asyncio 包的基础。
concurrent.futures 模块的主要特色是 ThreadPoolExecutor 和 ProcessPoolExecutor 类,这两个类实现的接口能分别在不同的线程或进程中执行可 调用的对象。这两个类在内部维护着一个工作线程或进程池,以及要执行的任务队列。
从 Python 3.4 起,标准库中有两个名为 Future 的类(实现了期物): concurrent.futures.Future 和 asyncio.Future 。这两个类的作用相同:这两 个类的实例都表示可能已经完成或者尚未完成的延迟计算。这与 Twisted 引擎中的 Deferred 类、Tornado 框架中的 Future 类,以及多个 JavaScript 库中的 Promise 对象类似。
期特封装待完成的操作,它们可以被存入队列,也可以通过它们查询操作的完成状态、获 取操作结果(或抛出的异常)...... 通常情况下客户端代码不应该创建期物,也不应该改 变期物的状态。并发框架会创建期特,并在期物表示的延迟计算结束后改会期物的状态。
期物都有 .done() 方法,这个方法不会阻塞,返回值是布尔值,指明期物链接的可调 用对象是否已经执行结束。但是客户端代码通常不会询问期物是否运行结束,而是会等待 通知。因此,两个 Future 类都有 .add_done_callback() 方法:这个方法只有 一个参数,类型是可调用的对象,期物运行结束后会调用指定的可调用对象。
期物的 .result() 方法在期物运行结束后可以返回任何的执行结果(或者重新抛出 任务执行期间抛出的异常)。
这两个标准库中有几个函数会返回期物,其它函数则直接使用期特,并为用户提供一种易 于理解的使用方式,例如, Executor.map 方法就属于后者:它返回一个迭代器,迭 代器的 __next__ 方法调用各个期物的 result 方法,因此我们得到的是各个期 物的结果,而不是期物本身。
CPython 解释器本身就不是线程安全的,因此有全局解释器锁(GIL),一次只允许使用一 个线程执行 Python 字节码。因此,一个 Python 进程通常不能同时使用多个 CPU 核心。 这是 CPython 解释器的局限,与 Python 语言无关。Jython 和 IronPython 没有这种限 制。不过,目前最快的 Python 解释器 PyPy 也有 GIL。
编写 Python 代码时无法控制 GIL;不过,执行耗时的任务时,可以使用一个内置的函数 或者一个使用 C 语言编写的扩展释放 GIL。其实,有个使用 C 语言编写的 Python 库能 管理 GIL,自行启操作系统线程,利用全部要用的 CPU 核心。这样做会极大地增加库代码 的复杂度,因此大多数库的作者都不这么做。
然而,标准库中所有执行阻塞型 I/O 操作的函数,在等待操作系统返回结果时都会释放 GIL。这意味着在 Python 语言这个层次上可以使用多线程,而 I/O 密集型 Python 程序 能从中受益:一个 Python 线程等待网络响应时,阻塞型 I/O 函数会释放 GIL,再运行一 个线程。
所以,Python 线程特别适合 I/O 密集型应用。同时,使用 concurrent.futures.ProcessPoolExecutor 类能轻松绕开 GIL。
杂项
- 为了高效处理网络 IO,需要使用并发,因为网络有很高的延迟,所以为了不浪费 CPU 周期去等待,最好在收到网络响应之前做些其他的事。
- executor.submit 和 futures.as_completed 的组合比 executor.map 更 灵活,因为 submit 方法能处理不同的可调用对象和参数,而 executor.map 只能处理参数不同的同一个可调用对象。此外,传给 futures.as_completed 函数 的期物集合可以来自多个 Executor 实例,例如一些由 ThreadPoolExecutor 实现创建,另一些由 ProcessPoolExecutor 实例创建。
- 对于 CPU 密集型和数据密集型并行处理,现在有个新工具可用————分布式计算引擎 Apache Spark。Spark 在大数据领域发展势头强劲,提供了友好的 Python API,支持把 Python 对象当作数据。
第 18 章 使用 asyncio 包处理并发
并发是指一次处理多件事。 并行是指一次做多件事。 二者不同,但是有联系。 一个关于结构,一个关于执行。 并发用于制定方案,用来解决可能(但未必)并行的问题。 ———— Rob Pike,Go 语言的创造者之一
摘录
本章介绍 asyncio 包,这个包使用事件循环驱动的协程实现并发。这是 Python 中最 大也是最具雄心壮志的库之一。Guido van Rossum 在 Python 仓库之外开发 asyncio 包,把这个项目的代号命名为 “Tulip”。Python 3.4 把 Tulip 添加到标准库中时,把它 重命名为 asyncio 。这个包也兼容 Python 3.3,但它大量使用了 yield from 表达式,因此与 Python 旧版不兼容。
Python 社区往往会忽略一个事实————访问本地文件系统会阻塞,想当然地认为这种操作不 会受网络访问的高延迟影响。与之相比,Node.js 程序员则始终谨记,所有文件系统函数 都会阻塞,因为这些函数的签名中指明了要有回调。硬盘 I/O 阻塞会浪费几百万个 CPU 周期,而这可能会对应用程序的性能产生重大影响。 asyncio 的事件循环在背后维护 着一个 ThreadPoolExecutor 对象,我们可以调用 run_in_executor 方法,把可 调用的对象发给它执行。
杂项
- Python 线程的调度程序在任何时候都能中断线程。必须使用锁去保护程序中的重要部 分,防止多步操作在执行过程中中断,而使数据无效。
第 19 章 动态属性和特性
特性至关重要的地方在于,特性的存在使得开发者可以非常安全并且确定可行地将公 共数据属性作为类的公共接口的一部分开放出来。 ———— Alex Martelli,Python 贡献者和图书作者
摘录
在 Python 中,数据的属性和处理数据的方法统称为 属性 (attribute)。其实,方 法只是可调用的属性。除了二者之外,我们还可以创建 特性 (property),在不改 变类接口的前提下,使用存取方法修改数据属性。这与 统一访问原则 相符:
不管服务是由存储还是计算实现的,一个模块提供的所有服务都应该通过统一的方式 使用。
除了特性,Python 还提供了丰富的 API,用于控制属性的访问权限,以及实现动态属性。 使用点号访问属性时(如 obj.attr ),Python 解释器会调用特殊的方法(如 __getattr__ 和 __setattr__ )计算属性。用户自定义的类可以通过 __getattr__ 方法实现 “虚拟属性”,当访问不存在的属性时,即时计算属性的值。
动态创建属性是一种元编程,框架的作者经常这么做 ...... 这个做法的关键技术是 __getattr__ 方法,Python 仅当无法使用常规的方式获取属性(即在实例、类或超类 中找不到指定的属性)时,才会调用该特殊方法。
我们通常把 __init__ 方法称为构造方法,这是从其他语言借鉴过来的术语。其实, 用于构建实例的是特殊方法 __new__ :这是个类方法(使用特殊方式处理,因此不必 使用 @classmethod 装饰器),它必须返回一个实例,这个返回的实例会作为 __init__ 方法的第一个参数。同时, __init__ 不返回任何值,所以 __init__ 方法其实是 “初始化方法”。我们几乎不需要自己编写 __new__ 方法, 从 object 类继承的实现已经足够了。 __new__ 方法也可以返回其他类的实例, 此时,解释器不会调用 __init__ 方法。
__new__ 是类方法,它的第一个参数是类本身,余下的参数与 __init__ 方法一 样,只不过没有 self 参数。
虽然内置的 property 经常用作装饰器,但它其实是一个类。在 Python 中,函数和 类通常可以互换,因为二者都是可调用的对象,而且没有实例化对象的 new 运算符, 所以调用构造方法与调用工厂函数没有区别。此外,只要能返回新的可调用对象,代替被 装饰的函数,二者都可以用作装饰器。
类中的特性能影响实例属性的寻找方式。特性都是类属性,但是特性管理的其实是实例属 性的存取。前面说过,如果实例和所属的类有同名数据属性,那么实例属性会遮盖类属性 ————至少通过那个实例读取属性时是这样。但是这个规则不适用于特性。特性不会被实例 属性遮盖。
杂项
其它使用属性的方式访问字典元素的实现:AttrDict( https://pypi.python.org/pypi/attrdict)和 addict( https://pypi.python.org/pypi/addict)。
使用 dict 处理函数参数,以确保传入的参数是字段(或者能转换成字段的对 象),同时创建的副本也能保证不误修改该参数。
keyword.iskeyword() 函数可以用来判断某个字符串是否是 Python 语言关键字。 Python 3 的 str 类提供的 s.isidentifier() 方法可以用来判断某个字符串 是否是有效的 Python 标识符。
shelve 模块提供了 pickle 的存储方式。
A "shelf" is a persistent, dictionary-like object. The difference with "dbm" databases is that the values in a shelf can be essentially arbitrary Python objects -- anything that the pickle module can handle.
我们可以使用如下方式快速的根据构造参数创建实例属性:
class Record(object): def __init__(self, **kwargs): self.__dict__.update(kwargs)
dir() 函数会列出对象的大多数属性。它的目的是交互式使用,因此没有提供完整 的属性列表,只列出一组 “重要的” 的属性名。 dir 函数能审查有或没有 __dict__ 属性的对象,它不会列出 __dict__ 属性本身,但会列出其中的键。 dir 函数也不会列出类的几个特殊属性,例如, __mro__ 、 __bases__ 和 __name__ 。
vars() 函数返回对象的 __dict__ 属性。
第 20 章 属性描述符
学会描述符之后,不仅有更多的工具集可用,还会对 Python 的运作方式有更深入的 理解,并由衷赞叹 Python 设计的优雅。 ———— Raymond Hettinger,Python 核心开发者和专家
摘录
描述符是对多个属性运用相同存取逻辑的一种方式。例如,Django ORM 和 SQLAlchemy 等 ORM 中的字段类型是描述符,把数据库记灵中字段里的数据与 Python 对象的属性对应起 来。描述符是实现了特定协议的类,这个协议包括 __get__ 、 __set__ 和 __delete__ 方法。特性(property)实现了完整的描述符协议。通常,可以只实现部 分协议。描述符是 Python 的独有特征,不仅在应用层中使用,在语言的基础设施中也有 用到。除了特性之外,方法 及 classmethod 和 staticmethod 装饰器都通过描 述符实现。
描述符的用法是,创建一个实例,作为另一个类的类属性。实现描述符协议的类叫 描述 符类 ,把描述符实例声明为类属性的类叫 托管类 ,托管类中由描述符实例处理的 公开属性,叫做 托管属性 。
__get__ 方法有三个参数: self 、 instance 和 owner 。 owner 是托管类的引用,通过描述符从托管类中获取属性时用和到。如果调用者使用托管类访问 托管属性,描述符的 __get__ 方法接收到的 instance 参数值是 None 。
如前如述,Python 存取属性的方式特别不对等。通过实例读取属性时,通常返回的是实例 中定义的属性;但是,如果实例中没有指定属性,那么会获取类属性。而为实例中的属性 赋值时,通常会在实例中创建属性,根本不影响类。
这种不对等的处理方式对描述符也有影响。其实,根据是否定义 __set__ 方法,描述 符可分为两大类。实现了 __set__ 方法的描述符属于 覆盖型描述符 ,因为虽然 描述符是类属性,但是实现 __set__ 方法的话,会覆盖对实例属性的赋值操作。特性 也是覆盖型描述符:如没有提供 setter 函数,特性的 __set__ 方法会抛出 AttributeError 异常,指明那个属性是只读的 ...... 没有实现 __set__ 方法 的描述符是 非覆盖型描述符 。如果设置了同名的实例属性,描述符会被遮盖,致使 描述符无法处理那个实例的那个属性。方法(method)是以非覆型描述符实现的。
如果覆盖型描述符没有实现 __get__ 方法的话,只有写操作由描述符处理。通过实 例读取描述符会返回描述符对象本身。如果实例在其 __dict__ 中有同名实例属性 时,写操作依然由描述符处理,但是通过实现读取这个名字的属性时,会返回实例属性。 也就是说,读操作时,实例属性会遮盖没有实现 __get__ 方法的覆盖型描述符。
另外,描述符都被定义为类属性,依附在类上的描述符无法控制为类属性赋值的操作。这 意味着为类属性赋值能覆盖描述符属性。这表示了读写属性的另一种不对等性:读类属性 的操作可以由依附在托管类上定义有 __get__ 方法的描述符处理,但是写类属性的操 作不会由依附在托管类上定义有 __set__ 方法的描述符处理。
若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。默认情 况下,对用户定义的类来说,其元类是 ``type`` ,而我们不能为 ``type`` 添加属 性。不过在第 21 章,我们会自己创建元类。
因为用户定义的函数都有 __get__ 方法,所以依附到类上,就相当于描述符。但是函 数并没有实现 __set__ 方法,因此是非覆盖型描述符。与描述符一样,通过托管类访 问时,函数的 __get__ 方法会返回自身的引用。但是通过类实例访问在时,函数的 __get__ 方法返回的是绑定方法对象:一种可调用的对象,里面包装着函数,并把托 管实例绑定给函数的第一个参数 ...... 绑定方法对象还有个 __call__ 方法,用于 处理真正的调用过程。这个方法会调用 __func__ 属性引用的原始函数,把函数的第 一个参数设为绑定方法的 __self__ 属性。这就是形参 self 的隐式绑定方法。
函数会变成绑定方法,这是 Python 语言底层使用描述符的最好例证。
描述符用法建议:
- 使用特性以保持简单
- 只读描述符必须有 __set__ 方法
- 用于验证的描述符可以只有 __set__ 方法
- 仅有 __get__ 方法的描述符可以实现高效缓存
- 非特殊的方法可以被实例属性遮盖 ———— 解释器只会在类中寻找特殊方法
杂项
为了给用户提供内省和其他元编程技术支持,通过类访问托管属性时,最好让 __get__ 方法返回描述符实例。例如:
def __get__(self, instance, owner): if instance is None: return self else: return getattr(instance, self.storage_name)
Python 函数对象本身都实现了 __get__ 方法,也就是说,函数本身都是 非覆盖 性描述符 。这样一来,函数如果被赋值给类属性的话,就可以作为方法使用了。
第 21 章 类元编程
(元类)是深奥的知识,99% 的用户都无需关注。如果你想知道是否需要使用元类, 我告诉你,不需要(真正需要使用元类的人确信他们需要,无需解释原因)。 ———— Tim Peters,Timsort 算法的发明人,活跃的 Python 贡献者
摘录
类元编程是指在运行时创建或定制类的技艺。在 Python 中,类是一等对象,因此任何时 候都可以使用函数新建类,而无需使用 class 关键字。类装饰器也是函数,不过能够 审查,修改,甚至把装饰的类替换成其它类。最后,元类是类元编程最高级的工具:使用 元类可以创建具有某种特质的全新类种,例如我们见过的抽象基类。
通常,我们把 type 视作函数,因为我们像函数那样使用它,例如,调用 type(my_object) 获取对象所属的类————作用与 my_object.__class__ 相同。然 而,type 是一个类。当成类使用时,传入三个参数可以新建一个类:
MyClass = type("MyClass", (MySuperClass, MyMixin),
{'x': 42, 'x2': lambda self: self.x * 2})
type 的三个参数分别是 name 、 bases 、和 dict 。最后一个参数是 映射,指定新类的属性名和值 ...... 把三个参数传给 type 是动态创建类的常用方 式。
为了正确地做元编程,你必须知道 Python 解释器什么时候计算各个代码块。Python 程序 员会区分 “导入时” 和 “运行时”,不过这两个术语没有严格的定义,而且二者之间存在着 灰色地带。在导入时,解释器会从上到下一次性解析完模块的代码,然后生成用于执行的 字节码。如果句法有错误,就在此时报告。如果本地的 __pycache__ 文件夹中有最新的 .pyc 文件,解释器会跳过上述步骤,因为已经有运行所需的字节码了。
编译肯定是导入时的活动,不过那个时期还会做些其它事,因为 Python 中的语句几乎都 是可执行的,也就是说语句可能会运行用户代码,修改用户程序的状态。尤其是 import 语句,它不只是声明,在进程中首次导入模块时,还会运行所导入模块中的全 部顶层代码————以后导入相同的模块则使用缓存,只做名称绑定。那些顶层代码可以做任 何事,包括通常在 “运行时” 做的事,例如连接数据库。因此,“导入时” 和 “运行时” 之 间的界线是模糊的: import 语句可以触发任何 “运行时” 行为。
在前一段中我写道,导入时会 “运行全部顶层代码”,但是 “顶层代码” 会经过一些加工。 导入模块时,解释器会执行顶层的 def 语句,可是这么做有什么作用呢?解释器会编 译函数的定义体(首次导入模块时),把函数对象绑定到对应的全局名称上,但是显然解 释器不会执行函数的定义体。通常这意味着解释器在导入时定义顶层函数,但是公当在运 行时调用函数时才会执行函数的定义体。
对类来说,情况就不同了:在导入时,解释器会执行每个类的定义体,甚至会执行嵌套类 的定义体。执行类定义体的结果是,定义了类的属性和方法,并构造了类对象。从这个意 义上理解,类的定义体属于 “顶层代码”,因为它在导入时运行。
元类是制造类的工厂。根据 Python 对象模型,类是对象,因此类肯定是另外某个类的实 例。默认情况下,Python 中的类是 type 类的实例。也就是说, type 是大多数 内置的类和用户定义的类的元类。为了避免无限回溯, type 是其自身的实体。而 type 是类,它就又是 object 的子类。
object 类和 type 类之间的关系很独特:object 是 type 的实例,而 type 是 object 类的子类。这种关系很 “神奇”,无法用 Python 代码表述,因为定义其中一 个之前另一个必须存在。type 是自身的实例这一点也很神奇。
除了 type ,标准库中还有一些别的元类,例如 ABCMeta 和 Enum ...... 向上追溯, ABCMeta 最终所属的类也是 type 。所有类都直接或间接地是 type 的实例,不过只有元类同时也是 type 的子类。元类从 type 类继承了 构建类的能力。具体来说,元类可以通过实现 __init__ 方法定制实例。元类的 __init__ 方法可以做到类装饰器能做到的任何事情,但是作用更大。
杂项
- Python 提供了充足的内省工具,大多数时候都不需要使用 exec 和 eval 函 数。
- type 构造方法及元类的 __new__ 和 __init__ 方法都会收到要计算的 类的定义体,形式是名称到属性的映像。
其它信息
- <Python 技术手册> 对属性访问机制的描述,应该是除了 CPython 中的 C 源码之外在 这方面最权威的解释。
- Python Tutor 是一个对 Python 运行原理进行可视 化分析的工具网站。
Comments
不要轻轻地离开我,请留下点什么...