Python风格的关键完全体现在Python的数据模型上,而数据模型所描述的API,为使用最地道的语言特性来构建开发者自己的对象提供了工具。当Python解析器遇到特殊句法时,会使用特殊方法去激活一些基本的对象操作。特殊方法以双下划线开头,以双下划线结尾(如:__getitem__)。如:obj[key]的背后就是__getitem__方法。魔术方法是特殊方法的昵称,特殊方法也叫双下方法。
一. 一摞Python风格的纸牌
使用__getitem__和__len__创建一摞有序的纸牌(使用collections.namedtuple构建了一个简单的类来表示一张纸牌,自python2.6开始,namedtuple就加入到python里,用以构建只有少数属性但没有方法的类):
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
# ♠, ♡, ♣, ♢,
suits = ['\u2660', '\u2661', '\u2663', '\u2662']
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, item):
return self._cards[item]
# 扑克牌排序:2最小,A最大,花色由大到小:♠, ♡, ♢,♣
suit_values = {'\u2660': 3, '\u2661': 2, '\u2662': 1, '\u2663': 0}
def sort_rule(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]
演示1 我们自定义的FrenchDeck类可以像任何python标准集合类型一样,可以使用len()函数;查看一叠牌有多少张:
演示2 d[0]或d[-1]由__getitem__方法提供:
演示3 random.choice方法用于从一个序列中随机选出一个元素;随机取出一张纸牌:
想必,现在你已经体会到通过python特殊方法来利用Python数据模型的2个好处:
- 作为类的用户,无需去记住标准操作的各种名词,如获取长度是.size,还是.length,还是别的什么?
- 可以更加方便地利用python的标准库,如random.choice函数。
演示4 因为__getitem__方法把[]操作交给了self.cards列表,所有我们的FrenchDeck实例自动支持切片:
演示5 仅仅实现了__getitem__方法,这一摞牌即可迭代:
反向迭代也OK:
演示6 迭代通常是隐式的,比如一个集合类型没有实现__contains__方法,那么in运算符就会按顺序做一次迭代搜索。因此,in运算符可以用在我们的FrenchDeck实例上,因为它是可迭代的:
演示7 使用python标准库中的sorted函数:
总结:虽然FrenchDeck隐式地继承了object类,但功能却不是继承而来的。通过实现__len__和__getitem__这两个特殊方法,FrenchDeck类就跟一个Python自有的序列数据类型一样,可以体现出Python的核心语言特性(例如迭代和切片)。同时这个类还可用于标准库中诸如random.choice、reversed和sorted这些函数。
演示8 按照目前的设计,FrenchDeck还不支持洗牌,因为它是不可变的:
错误消息相当明确:对象不支持元素赋值。原因是,shuffle函数要调换集合中元素的位置,而FrenchDeck只实现了不可变的序列协议。可变的序列还必须提供__setitem__方法。
演示9 为FrenchDeck打猴子补丁,把它变成可变的,让random.shuffle函数能处理:
备注:Python是动态语言,因此我们可以在运行时添加__setitem__方法,甚至是在交互式控制台中添加。演示9中把set_card函数赋值给特殊方法__setitem__,从而把它依附到FrenchDeck类上。这种技术叫猴子补丁 :在运行时修改类或模块,而不改动源码。猴子补丁很强大,但猴子补丁的代码与要打补丁的程序耦合十分紧密。
定义set_card函数时使用的参数为deck,position,card;而在类中创建__setitem__方法默认使用的参数是self,key和value。此处这样定义,只是为了说明每个Python方法说到底都是普通函数,把第一个参数命名为self只是一种约定。在控制台会话中使用那几个参数没问题,不过在Python源码文件中最好按照文档那样使用self、key和value。
二. 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被Python解析器调用的。即没有obj.__len__()这种写法,而是len(obj)。在执行len(obj)时,如果obj是一个自定义类的对象,那么python会自己去调用我们实现的__len__方法。
然而如果是python内置类型,比如列表、字符串。字节序列等,那么CPython会抄个近路,__len__实际上会返回PyVarObject里的ob_size属性;直接读取这个值比调用一个方法要快得多。很多时候,特殊方法的调用是隐式的,比如for i in x:这个语句其实是调用iter(x),而这个函数的背后是x.__iter__()方法。
通过内置函数(如:len、iter、str等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用这些方法,通常还提供额外的好处,而且对于内置类型来说,它们的速度更快。
2.1 模拟数值类型
一个简单的二维向量类
from math import hypot
class Vector:
"""自定义二维向量"""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f'Vector({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, scaler):
return Vector(self.x * scaler, self.y * scaler)
演示1 自定义二维向量的使用
备注:python内置的repr函数能把一个对象用字符串的形式表达出来以便辨认,这就是字符串表示形式。repr是通过调用__repr__这个特殊方法来得到一个对象的字符串表示形式的。交互式控制台和调试程序用repr函数来获取字符串表示形式。__repr__和__str__的区别在于,后者是在str()函数被调用或是在用print函数打印一个对象的时候才调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这2个特殊方法中的一个,__repr__是更好的选择,因为如果一个对象没有__str__函数,而python又需要调用它的时候,解析器会调用__repr__作为替代。
通过__add__和__mul__为向量类带来了+和*两个运算符。此外还可以使用__radd__和__rmul__实现交换律。
默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对__bool__或者__len__函数有自己的实现。bool(x)的背后是调用x.__bool__()的结果;如果不存在__bool__方法,那么bool(x)会调用x.__len__()。若返回0,则bool会返回False;否则返回True。
2.2 特殊方法一览
类别 | 方法名 |
字符串/字节序列表示形式 | __repr__、__str__、__format__、__bytes__ |
数值转换 | __abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__ |
集合模拟 | __len__、__getitem__、__setitem__、__delitem__、__contains__ |
迭代枚举 | __iter__、__reversed__、__next__ |
可调用模式 | __call__ |
上下文管理 | __enter__、__exit__ |
实例的创建和销毁 | __nex__、__init__、__del__ |
属性管理 | __getattr__、__getattribute__、__setattr__、__delattr__、__dir__ |
属性描述符 | __get__、__set__、__delete__ |
跟类相关的服务 | __prepare__、___instancecheck__、__subclasscheck__ |
类别 | 方法名和对应的运算符 |
一元运算符 | __neg__ -、__pos__ +、__abs__ abs() |
比较运算符 | __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >= |
算数运算符 | __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmode()、__pow__ **/pow()、__round__ round() |
反向算数运算符 | __radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__、 |
增量赋值算术运算符 | __iadd__、__isub__、__imul__、__itruediv__、__ifloordiv、__imod__、__ipow__、、、 |
位运算符 | __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |、__xor__ ^ |
反向位运算符 | __rlshift__、__rrshift__、__rand__、__ror__、__rxor__ |
增量赋值位运算符 | __ilshift__、__irshift__、__iand__、__ior__、__ixor__ |