数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。
__getitem__
)。比如 obj[key]
的背后就是 __getitem__
方法,为了能求得 my_collection[key]
的值,解释器实际上会调用 my_collection.__getitem__(key)
。
这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:
- 迭代
- 集合类
- 属性访问
- 运算符重载
- 函数和方法的调用
- 对象的创建和销毁
- 字符串表示形式和格式化
- 管理上下文(即
with
块)
1.1 一摞Python风格的纸牌
接下来我会用一个非常简单的例子来展示如何实现 __getitem__
和 __len__
import collections
# 用 collections.namedtuple 构建了一个简单的类来表示一张纸牌。
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
# 牌面:2、3、...、A
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
# 花色:黑、红、花、块
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
# 生成一副牌
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]
def __len__(self):
# 返回牌数
return len(self._cards)
def __getitem__(self, position):
# 获取指定索引的牌
return self._cards[position]
if __name__ == '__main__':
deck = FrenchDeck()
# 查看一叠牌有多少张, 实际上是由 len() 调用 deck.__len__() 实现的。
print('总牌数:', len(deck))
# 抽取特定的一张牌, 实际上是由 [] 运算符调用 deck.__getitem__(position) 实现的。
print('最后一张牌:', deck[-1])
# __getitem__ 支持随机抽取一张牌
print('随机抽取一张牌:', choice(deck))
# __getitem__ 方法把 [] 操作交给了 self._cards 列表,所以 deck 类自动支持切片
print('切片:', deck[:3])
# __getitem__ 支持可迭代/反向迭代
for card in reversed(deck):
print(card)
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
很多时候,特殊方法的调用是隐式的,比如 for i in x:
这个语句,背后其实用的是 iter(x)
,而这个函数的背后则是 x.__iter__()
方法。当然前提是这个方法在 x
中被实现了。
通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现它们的次数。唯一的例外可能是 __init__
方法,你的代码里可能经常会用到它,目的是在你自己的子类的 __init__
示例 1 一个简单的二维向量类:
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)
下面我们通过这个类来讲些这些特殊方法。
1.2.1 字符串表示形式
Python 有一个内置的函数叫
repr
,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr
就是通过__repr__
这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现__repr__
,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是<Vector object at 0x10e100070>
。
__repr__
和 __str__
的区别在于,后者是在 str()
函数被使用,或是在用 print
如果你只想实现这两个特殊方法中的一个,__repr__
是更好的选择,因为如果一个对象没有 __str__
函数,而 Python 又需要调用它的时候,解释器会用 __repr__
1.2.2 算术运算符
通过 __add__
和 __mul__
,示例 1-2 为向量类带来了 +
和 *
这两个算术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self
或 other
)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。
1.2.3 自定义的布尔值
尽管 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
。
1.3 特殊方法一览
表1-1:跟运算符无关的特殊方法
类别 | 方法名 |
字符串 / 字节序列表示形式 |
|
数值转换 |
|
集合模拟 |
|
迭代枚举 |
|
可调用模拟 |
|
上下文管理 |
|
实例创建和销毁 |
|
属性管理 |
|
属性描述符 |
|
跟类相关的服务 |
|
表1-2:跟运算符相关的特殊方法
类别 | 方法名和对应的运算符 |
一元运算符 |
|
众多比较运算符 |
|
算术运算符 |
|
反向算术运算符 |
|
增量赋值算术运算符 |
|
位运算符 |
|
反向位运算符 |
|
增量赋值位运算符 |
|
⚠️ 当交换两个操作数的位置时,就会调用反向运算符(
b * a
而不是a * b
)。增量赋值运算符则是一种把中缀运算符变成赋值运算的捷径(a = a * b
就变成了a *= b
)。