Guido 对语言设计美学的深入理解让人震惊。我认识不少很不错的编程语言设计者,他们设计出来的东西确实很精彩,但是从来都不会有用户。Guido 知道如何在理论上做出一定妥协,设计出来的语言让使用者觉得如沐春风,这真是不可多得。
——Jim Hugunin
Jython 的作者,AspectJ 的作者之一,.NET DLR 架构师
Python 最好的品质之一是一致性:你可以轻松理解 Python 语言,并通过 Python 的语言特性在类上定义规范的接口,来支持 Python 的核心语言特性,从而写出具有“Python 风格”的对象。
Python 解释器在碰到特殊的句法时,会使用特殊方法(我们称之为魔术方法)去激活一些基本的对象操作。
__getitem__
以双下划线开头的特殊方法,称为 dunder-getitem。特殊方法也称为双下方法(dunder-method)
如 my_c[key]
语句执行时,就会调用 my_c.__getitem__
函数。这些特殊方法名能让你自己的对象实现和支持一下的语言构架,并与之交互:
- 迭代
- 集合类
- 属性访问
- 运算符重载
- 函数和方法的调用
- 对象的创建和销毁
- 字符串表示形式和格式化
- 管理上下文(即
with
块)
实现一个 Pythonic 的牌组
1 | # 通过实现魔术方法,来让内置函数支持你的自定义对象 |
可以容易地获得一个纸牌对象
1 | beer_card = Card('7', 'diamonds') |
和标准 Python 集合类型一样,使用 len()
查看一叠纸牌有多少张
1 | deck = FrenchDeck() |
可选取特定一张纸牌,这是由 __getitem__
方法提供的
1 | # 实现 __getitem__ 以支持下标操作 |
随机抽取一张纸牌,使用 python 内置函数 random.choice
1 | from random import choice |
实现特殊方法的两个好处:
- 对于标准操作有固定命名
- 更方便利用 Python 标准库
__getitem__
方法把 [] 操作交给了 self._cards
列表,deck 类自动支持切片操作
1 | deck[12::13] |
同时 deck 类支持迭代
1 | for card in deck: |
迭代通常是隐式的,如果一个集合没有实现 __contains__
方法,那么 in 运算符会顺序做一次迭代搜索。
1 | Card('Q', 'hearts') in deck |
False
进行排序,排序规则:
2 最小,A最大。花色 黑桃 > 红桃 > 方块 > 梅花
1 | card.rank |
'A'
1 | suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) |
FrenchDeck 继承了 object 类。通过 __len__
, __getitem__
方法,FrenchDeck和 Python 自有序列数据类型一样,可体现 Python 核心语言特性(如迭代和切片),
Python 支持的所有魔术方法,可以参见 Python 文档 Data Model 部分。
比较重要的一点:不要把 len
,str
等看成一个 Python 普通方法:由于这些操作的频繁程度非常高,所以 Python 对这些方法做了特殊的实现:它可以让 Python 的内置数据结构走后门以提高效率;但对于自定义的数据结构,又可以在对象上使用通用的接口来完成相应工作。但在代码编写者看来,len(deck)
和 len([1,2,3])
两个实现可能差之千里的操作,在 Python 语法层面上是高度一致的。
如何使用特殊方法
特殊方法的存在是为了被 Python 解释器调用
除非大量元编程,通常代码无需直接使用特殊方法
通过内置函数来使用特殊方法是最好的选择
模拟数值类型
实现一个二维向量(Vector)类
1 | from math import hypot |
Vector 类中 6 个方法(除 __init__
外)并不会在类自身的代码中调用。一般只有解释器会频繁调用这些方法
字符串的表示形式
内置函数 repr, 通过 __repr__
特殊方法来得到一个对象的字符串表示形式。
算数运算符
通过 __add__
, __mul__
, 向量类能够操作 + 和 * 两个算数运算符。
运算符操作对象不发生改变,返回一个产生的新值
自定义的布尔值
- 任何对象可用于需要布尔值的上下文中(if, while 语句, and, or, not 运算符)
- Python 调用 bool(x) 判定一个值 x,bool(x) 只能返回 True 或者 False
- 如果类没有实现
__bool__
,则调用__len__
, 若返回 0,则 bool 返回 False
特殊方法一览
为何 len 不是普通方法
“实用胜于纯粹“
The Zen of Python
为了让 Python 自带的数据结构走后门, CPython 会直接从结构体读取对象的长度,而不会调用方法.
这种处理方式在保持内置类型的效率和语言一致性保持了一个平衡.
小结
- 通过实现特殊方法,自定义数据类型可以像内置类型一样处理
- 合理的字符串表示形式是Python对象的基本要求。
__repr__
,__str__
- 序列类型的模拟是特殊方法最常用的地方