本文的代码例子:
https://github.com/ccc013/CodesNotes/blob/master/FluentPython/1_Python%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B.ipynb
数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。
通常在不同框架下写程序,都需要花时间来实现那些会被框架调用的方法,python 当然也包含这些方法,当 python 解释器碰到特殊的句法的时候,会使用特殊方法来激活一些基本的对象操作,这种特殊方法,也叫做魔术方法(magic method),通常以两个下划线开头和结尾,比如最常见的 __init__
, __len__
以及 __getitem__
等,而 obj[key]
这样的操作背后的特殊方法是 __getitem__
,初始化一个类示例的时候,如 obj= Obj()
的操作背后,特殊方法就是 __init__
。
通过实现 python 的这些特殊方法,可以让自定义的对象实现和支持下面的操作:
接下来尝试自定义一个类,并实现两个特殊方法:__getitem__
和 __len__
,看看实现它们后,可以对自定义的类示例实现哪些操作。
这里自定义一个纸牌类,并定义了数字和花色,代码如下所示:
import collections
# 用 nametuple 构建一个类来表示纸牌
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]
其中辅助用到 collections
库的 nametuple
,用来表示一张纸牌,其属性包括数字 rank
和 花色 suit
,下面是对这个 Card
的简单测试:
# 测试 Card
beer_card = Card('7', 'diamonds')
beer_card
接着就是测试自定义的 FrenchDeck
类,这里会调用 len()
方法看看一摞纸牌有多少张:
# 测试 FrenchDeck
deck = FrenchDeck()
len(deck)
然后是进行索引访问的操作,这里测试从正序访问第一张,以及最后一张纸牌的操作:
print(deck[0], deck[-1])
如果想进行随机抽取卡牌,可以结合 random.choice
来实现:
# 随机抽取,结合 random.choice
from random import choice
choice(deck)
由于我们实现 __getitem__
方法是获取纸牌,所以也可以支持切片(slicing)的操作,例子如下所示:
# 切片
print(deck[:3])
print(deck[12::13])
另外,实现 __getitem__
方法就可以支持迭代操作:
# 可迭代的读取
for card in deck:
print(card)
反向迭代也自然可以做到:
# 反向迭代
for card in reversed(deck):
print(card)
break
另外,当然也可以自定义排序规则,如下所示:
# 制定排序的规则
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]
# 对卡牌进行升序排序
for card in sorted(deck, key=spades_high):
print(card)
总结一下,实现 python 的特殊方法的好处包括:
len()
方法,而不会是 size
或者 length
random.choice
、reversed
、sorted
,不需要自己重新发明轮子这里分两种情况来说明对于特殊方法的调用:
__len__
实际上会直接返回 PyVarObject
里的 ob_size
属性。PyVarObject
是表示内存中长度可变的内置对象的 C 语言结构体,直接读取这个值比调用一个方法要快很多。len, iter, str
等)调用特殊方法是最好的选择。对于特殊方法的调用,这里还要补充说明几点:
my_object.__len__()
,而应该是 len(my_object)
,这里的 my_object
表示一个自定义类的对象。for i in x
循环语句是用 iter(x)
,也就是调用 x.__iter__()
方法。接下来是实现一个自定义的二维向量类,然后自定义加号的特殊方法,实现运算符重载。
代码例子如下所示:
# 一个简单的二维向量类
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)
这里除了必须实现的 __init__
外,还实现了几个特殊方法:
__add__
: 加法运算符;__bool__
:用于判断是否真假,也就是在调用bool()
方法;默认情况下是自定义类的实例总是被认为是真的,但如果实现了 __bool__
或者 __len__
,则会返回它们的结果,bool()
首先尝试返回 __bool__
方法,如果没有实现,则会尝试调用 __len__
方法__mul__
:实现的是标量乘法,即向量和数的乘法;__abs__
:如果输入是整数或者浮点数,返回输入值的绝对值;如果输入的是复数,返回这个复数的模;如果是输入向量,返回的是它的模;__repr__
: 可以将对象用字符串的形式表达出来;这里要简单介绍下 __repr__
和 __str__
两个方法的区别:
__repr__
:交互式控制台、调试程序(debugger)、%
和 str.format
方法都会调用这个方法来获取字符串形式;__str__
:主要是在 str()
和 print()
方法中会调用该方法,它返回的字符串会对终端用户更加友好;__repr__
是更好的选择,因为默认会调用 __repr__
方法。接下来就是简单测试这个类,测试结果如下所示:
下面分别根据是否和运算符相关分为两类的特殊方法:
__repr__, __str__,__format__,__bytes__
__neg__ -, __pos__ +,__abs__ abs()
这里有两类运算符要解释一下:
b * a
而不是 a * b
;a *= b
的操作len
之所以不是普通方法,是为了让 Python 自带的数据结构变得高效,前面也提到内置类型在使用 len
方法的时候,CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法,因此速度会非常快。而在 python 的内置类型,比如列表 list、字符串 str、字典 dict 等查询数量是非常常见的操作。
这种处理方式实际上是在保持内置类型的效率和保证语言的一致性之间找到一个平衡点。
本文介绍了两个代码例子,说明了在自定义类的时候,实现特殊方法,可以实现和内置类型(比如列表、字典、字符串等)一样的操作,包括实现迭代、运算符重载、打印类实例对象等,然后还根据是否和运算符相关将特殊方法分为两类,并列举出来了,最后也介绍了 len
方法的例子来说明 python 团队是如何保持内置类型的效率和保证语言一致性的。