- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
1.2 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调用其中由你实现的 __len__ 方法。
然而如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构体。直接读取这个值比调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。
通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现它们的次数。唯一的例外可能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自己的子类的 __init__ 方法中调用超类的构造器。
通过内置的函数(例如 len、iter、str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。14.12 节中有详细的例子。
不要自己想当然地随意添加特殊方法,比如 __foo__ 之类的,因为虽然现在这个名字没有被 Python 内部使用,以后就不一定了。
1.2.1 模拟数值类型
利用特殊方法,可以让自定义对象通过加号“+”(或是别的运算符)进行运算。第 13 章对此有详细的介绍,现在只是借用这个例子来展示特殊方法的使用。
我们来实现一个二维向量(vector)类,这里的向量就是欧几里得几何中常用的概念,常在数学和物理中使用的那个(见图 1-1)。
图 1-1:一个二维向量加法的例子,Vector(2,4) + Vextor(2,1) = Vector(4,5)
Python 内置的 complex 类可以用来表示二维向量,但我们这个自定义的类可以扩展到 n 维向量,详见第 14 章。
为了给这个类设计 API,我们先写个模拟的控制台会话来做 doctest。下面这一段代码就是图 1-1 所示的向量加法:
>>> v1 = Vector(2, 4) >>> v2 = Vector(2, 1) >>> v1 + v2 Vector(4, 5)
注意其中的 + 运算符所得到的结果也是一个向量,而且结果能被控制台友好地打印出来。
abs 是一个内置函数,如果输入是整数或者浮点数,它返回的是输入值的绝对值;如果输入是复数(complex number),那么返回这个复数的模。为了保持一致性,我们的 API 在碰到 abs 函数的时候,也应该返回该向量的模:
>>> v = Vector(3, 4) >>> abs(v) 5.0
我们还可以利用 * 运算符来实现向量的标量乘法(即向量与数的乘法,得到的结果向量的方向与原向量一致 6,模变大):
6如果向量与负数相乘,得到的结果向量的方向与原向量相反。——编者注
>>> v * 3 Vector(9, 12) >>> abs(v * 3) 15.0
示例 1-2 包含了一个 Vector 类的实现,上面提到的操作在代码里是用这些特殊方法实现的:__repr__、__abs__、__add__ 和 __mul__。
示例 1-2 一个简单的二维向量类
from math import hypot class Vector: def __init__(self, x=0, y=0): self.x = x self.y = y def __repr__(self): 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)
虽然代码里有 6 个特殊方法,但这些方法(除了 __init__)并不会在这个类自身的代码中使用。即便其他程序要使用这个类的这些方法,也不会直接调用它们,就像我们在上面的控制台对话中看到的。上文也提到过,一般只有 Python 的解释器会频繁地直接调用这些方法。接下来看看每个特殊方法的实现。
1.2.2 字符串表示形式
Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector object at 0x10e100070>。
交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;在老的使用 % 符号的字符串格式中,这个函数返回的结果用来代替 %r 所代表的对象;同样,str.format 函数所用到的新式字符串格式化语法也是利用了 repr,才把 !r 字段变成字符串。
% 和 str.format 这两种格式化字符串的手段在本书中都会使用。其实整个 Python 社区都在同时使用这两种方法。个人来讲,我越来越喜欢 str.format 了,但是 Python 程序员更喜欢简单的 %。因此,这两种形式并存的情况还会持续下去。
在 __repr__ 的实现中,我们用到了 %r 来获取对象各个属性的标准字符串表示形式——这是个好习惯,它暗示了一个关键:Vector(1, 2) 和 Vector('1', '2') 是不一样的,后者在我们的定义中会报错,因为向量对象的构造函数只接受数值,不接受字符串 7。
7实际上,Vector 的构造函数接受字符串。而且,对于使用字符串构造的 Vector,这 6 个特殊方法中,只有 __abs__ 和 __bool__ 会报错。此外,1.2.4 节定义的 __bool__ 不会报错。——编者注
__repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象。因此这里使用了类似调用对象构造器的表达形式(比如 Vector(3, 4) 就是个例子)。
__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。
“Difference between __str__ and __repr__ in Python”是 Stack Overflow 上的一个问题,Python 程序员 Alex Martelli 和 Martijn Pieters 的回答很精彩。
1.2.3 算术运算符
通过 __add__ 和 __mul__,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或 other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。第 13 章会谈到更多这方面的问题。
示例 1-2 只实现了数字做乘数、向量做被乘数的运算,乘法的交换律则被忽略了。在第 13 章里,我们将利用 __rmul__ 解决这个问题。
1.2.4 自定义的布尔值
尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如 if 或 while 语句,或者 and、or 和 not 运算符)。为了判定一个值 x 为真还是为假,Python 会调用 bool(x),这个函数只能返回 True 或者 False。
默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对 __bool__ 或者 __len__ 函数有自己的实现。bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则返回 True。
我们对 __bool__ 的实现很简单,如果一个向量的模是 0,那么就返回 False,其他情况则返回 True。因为 __bool__ 函数的返回类型应该是布尔型,所以我们通过 bool(abs(self)) 把模值变成了布尔值。
在 Python 标准库的文档中,有一节叫作“Built-in Types”,其中规定了真值检验的标准。通过实现 __bool__,你定义的对象就可以与这个标准保持一致。
如果想让 Vector.__bool__ 更高效,可以采用这种实现:
def __bool__(self): return bool(self.x or self.y)
它不那么易读,却能省掉从 abs 到 __abs__ 到平方再到平方根这些中间步骤。通过 bool 把返回类型显式转换为布尔值是为了符合 __bool__ 对返回值的规定,因为 or 运算符可能会返回 x 或者 y 本身的值:若 x 的值等价于真,则 or 返回 x 的值;否则返回 y 的值。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论