数据模型其实是对Python框架的概述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。

不管在哪一种框架下写代码,都会花费大量时间去实现那些会被框架本身调用的方法,Python也不例外。Python解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作。

特殊方法一般用两个下划线开头,用两个下划线结尾。例如 __getitem__方法,obj[key]背后就是这个方法。比如为了求得 my_collection的值,解释器实际上调用的是my_collection[key].__getitem__(key)。

下面引出一个例子,来说明 __getitem__和 __len__这两个特殊方法。

下面是一个纸牌类:

# 一摞有序的纸牌
import collections
from random import choice
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
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]

然后输入:

# 测试输出
beer_card = Card('7', 'diamonds')
print(beer_card)
会得到:
Card(rank='7', suit='diamonds')
也可以用:
deck = FrenchDeck()
print(len(deck))

用上述代码可以查看一叠牌有多少张。

随机抽取纸牌也可以用内置的方法,Python的内置函数 random.choice可以直接用在这一摞纸牌实例上。

于是你就会发现,实现特殊方法利用Python的数据模型的好处:作为你的类的用户,他们不必去记住标准操作的各式名称(例如得到函数总数是.size()还是.length()还是别的什么函数。)。

可以直接用Python标准库,比如random.choice函数,而不用重新造轮子。

其他的函数你也可以使用,比如reversed和sorted这些函数。对合成的应用可以让__len__和__getitem__方法的具体实现可以代理给self._cards这个Python列表(list对象)。

下面说一下如何使用特殊方法:

特殊方法的存在是为了被解释器调用的,自己并不需要调用他们。并不存在,my_object.__len__()这种写法。应该使用len(my_object)。如果my_object是一个自定义类的对象,那么Python回去调用由你实现的__len__方法。

很多时候特殊方法的调用都是隐式的,比如for i in x:这一个语句背后用的是iter(x)。函数背后则是x.__iter__()方法。前提是这个方法被实现了。

一般来说,直接调用特殊方法的次数会比较少,除非有大量的元编程存在。或者是__init__方法。

用内置函数会有额外的效果,内置的类速度也会更快。

然后我们在这里引入一个例子:

利用“+”实现向量加法的运算。

然后利用__repr__、__abs__、__add__和__mul__特殊方法可以实现。

下面是示例代码:

# 实现一个简单的二维向量类
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))
# 更高效的bool方法
def __bool__(self):
return bool(self.x or self.y)
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)
然后输入测试用例:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
v = v1 + v2
print(v)

然后会返回结果:

Vector(4, 5)

这里面虽然有6个特殊的方法,但是除了__init__方法,其他方法并不会在这个类自身的代码中使用。一般只有Python的解释器会频繁的直接调用这些方法。

下面说其中的特殊方法的实现,第一个是字符串表示形式:

Python有个内置函数是repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 。

在 __repr__ 的实现中,用到了 %r 来获取对象各个属性的标准字符串表示形式——

这是个好习惯,它暗示了一个关键:Vector(1, 2) 和 Vector('1', '2') 是不一样的,后者在我们的定义中会报错,因为向量对象的构造函数只接受数值,不接受字符串。

然后说一下__repr__和__str__的区别,后者是在str()函数被人使用,或者是在用print函数打印一个对象的时候才会被调用,返回的字符串会更友好一点。

如果想实现这两个特殊方法其中的一个的话,__repr会是更好的选择,因为一个对象如果没有__str__函数,儿Python又需要调用它的时候,解释器会用__repr__代替。

然后说第二个实现,算术运算符:

上面的例子,通过__add__和__mul__,带来了“+”“*”两个算术运算符。值得注

意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或

other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不

改变操作对象,而是产出一个新的值。

然后说最后一个实现,自定义的bool值:

尽管 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和Python的官方文档):

然后是跟运算符相关的特殊方法:

说到这里,文章就快结束了。通过实现特殊方法,自定义数据类型可以表现的跟内置类型一样,从而让我们写出更具有表达力的代码。或者说更具有Python风格的代码。

Python中对象的一个基本要求就是它得有合理的字符串的表示形式,而__repr__和__str__就很满足这个要求,前者用来调试,后者用来给终端用户看。

Python通过运算符重载这个模式提供了很多的数值类型,有兴趣的话,可以查一下相关的资料。

另外,Python数据模型另外的一个写法就是Python对象模型,而特殊方法在某些语言中经常也被叫做魔术方法。

另外一个名词就是元对象,元对象是指那些对构建语言本身来讲很重要的对象,以此为前提,元对象协议中的协议可以看做接口,也就是说,元对象协议是对象模型的同义词,他们的意思都是构建核心语言的API。

讲到这里,文章就结束了。谢谢大家关注。

笔者,最近得了重感冒。一直咳嗽,过了一周了,还没有变好的趋势。

大家一定注意身体,多喝热水真的不是说说。