目录
特殊方法
1.1一撂Python风格的纸牌
1.2如何使用特殊方法
1.3特殊方法一览
1.4为什么len不是普通方法
1.5本章小结
特殊方法以两个下划线开头,两个下划线结尾(例如__getitem__)。
比如obj[key]的背后就是__getitem__方法,为了能求得my_collection[key]的值,python解释器实际调用my_collection.__getitem__(key)。
特殊方法也叫双下方法(dunder method)。
接下来用一个简单的例子展示如何实现__getitem__和__len__这两个特殊方法。
首先利用collections.namedtuple建立一个纸牌类:
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]
需求1:查看一叠牌有多少张
实现:__len__方法
需求2:从一叠牌中抽取特定的一张
实现:__getitem__方法,还能使对象变得可迭代,支持切片操作。
需求3:随机抽取一张纸牌
实现:需要单独写一个方法吗?不需要,使用random.choice即可。
from random import choice print(choice(deck))
现在已经可以体会到特殊方法的两个好处:
①用户不必记住标准的各式名称(“怎么得到元素的总数?是.size()还是.length()还是别的什么?”)。
②更加方便地利用Python的标准库,例如random.choice函数,而不必重新发明轮子。
再看看以下需求的实现。
需求4:给纸牌排序
实现:定义排序规则函数
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)
需求5:如何洗牌
实现:打猴子补丁,在运行时修改类或模块,而不修改源码
from random import shuffle def set_card(deck,position,card): deck._cards[position]=card FrenchDeck.__setitem__=set_card shuffle(deck) print(deck[:5])
首先明确,特殊方法是为了给python解释器调用的。
例如在执行len(my_object)的时候,如果my_object是自定义数据类型对象,那么python会调用你实现的__len__方法。
然而如果是python内置类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么python解释器会抄个近路,__len__实际上会返回PyVarObject的ob_size属性。PyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值显然比调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的。比如for i in x:这个语句,背后其实用的是iter(x),而这个函数背后则是x.__iter__()方法。当然前提是这个方法在x中被实现了。
通常无需直接使用特殊方法,除非有大量的元编程存在。唯一的例外是__init__方法,你的代码里可能会经常使用。
另外,不要想当然地随意添加特殊方法,例如__foo__之类的,因为虽然现在这个名字没有被Python内部使用,以后就不一定了。
1.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 '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) v1=Vector(2,4) v2=Vector(2,1) print('v1+v2=',v1+v2) print('abs(v1)=',abs(v1)) print('v2*3=',v2*3)
1.2.2 字符串表示形式
Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是
。 交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;在老的使用 % 符号的字符串格式中,这个函数返回的结果用来代替 %r 所代表的对象;同样,str.format 函数所用到的新式字符串格式化语法也是利用了 repr,才把 !r 字段变成字符串。
__repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象。因此这里使用了类似调用对象构造器的表达形式(比如 Vector(3, 4) 就是个例子)。
__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。
1.2.3 算术运算符
通过 __add__ 和 __mul__,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或 other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。
乘法交换律将用__rmul__解决。
1.2.4 自定义的布尔值
尽管 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 标准库的文档中,有一节叫作“Built-in Types”,其中规定了真值检验的标准。通过实现 __bool__,你定义的对象就可以与这个标准保持一致。
如果想让 Vector.__bool__ 更高效,可以采用这种实现:
def __bool__(self): return bool(self.x or self.y)
它不那么易读,却能省掉从 abs 到 __abs__ 到平方再到平方根这些中间步骤。通过 bool 把返回类型显式转换为布尔值是为了符合 __bool__ 对返回值的规定,因为 or 运算符可能会返回 x 或者 y 本身的值:若 x 的值等价于真,则 or 返回 x 的值;否则返回 y 的值。
具体参考Python 语言参考手册中的“Data Model”。
”Python之禅“:“实用胜于纯粹。” “不能让特例特殊到开始破坏既定规则。”
换句话说,len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门,abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点。
通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具 Python 风格的代码。
Python 对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过 __repr__ 和 __str__ 来满足这个要求。前者方便我们调试和记录日志,后者则是给终端用户看的。这就是数据模型中存在特殊方法 __repr__ 和 __str__ 的原因。
对序列数据类型的模拟是特殊方法用得最多的地方,这一点在 FrenchDeck 类的示例中有所展现。在第 2 章中,我们会着重介绍序列数据类型,然后在第 10 章中,我们会把 Vector 类扩展成一个多维的数据类型,通过这个练习你将有机会实现自定义的序列。
Python 通过运算符重载这一模式提供了丰富的数值类型,除了内置的那些之外,还有 decimal.Decimal 和 fractions.Fraction。这些数据类型都支持中缀算术运算符。在第 13 章中,我们还会通过对 Vector 类的扩展来学习如何实现这些运算符,当然还会提到如何让运算符满足交换律和增强赋值。
Python 数据模型的特殊方法还有很多,本书会涵盖其中的绝大部分,探讨如何使用和实现它们。