流畅的python——1 数据模型
一、数据模型
当python解释器遇到特殊的句法时,会调用特殊方法,比如:d['a']
会调用 __getitem__
magic 和 dunder:魔法方法 magic method
是特殊方法的昵称。特殊方法也叫 双下方法 dunder method
。
具名元组 namedtuple
:用于构建只有少数属性,但是,没有方法的对象。
import collections
Card = collections.namedtuple('Card',['rank','suit'])
特殊方法:__len__
和 __getitem__
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): # 调用 len() ,触发 __len__ 特殊方法
return len(self._cards)
def __getitem__(self, position): # 调用 f[0] ,触发 __getitem__ 方法
return self._cards[position]
In [6]: f = F()
In [8]: len(f)
Out[8]: 52
In [9]: f[0]
Out[9]: Card(rank='2', suit='spades')
random.choice 从序列中随机取一个元素,注意:此时的 f 是一个对象
In [11]: random.choice(f)
Out[11]: Card(rank='10', suit='spades')
In [12]: f
Out[12]: <__main__.F at 0x2a3d7ee3588>
In [13]: for i in f:
...: print(i)
...:
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
Card(rank='5', suit='spades')
Card(rank='6', suit='spades')
Card(rank='7', suit='spades')
Card(rank='8', suit='spades')
Card(rank='9', suit='spades')
Card(rank='10', suit='spades')
...
因为 __getitem__
方法把 []
操作交给了 self._card
列表,所以,f 自动支持切片操作,即可迭代,迭代 f 对象,得到的结果是迭代 self._card
列表。
__getitem__
实现该特殊方法:[]
和 可迭代
迭代是隐式的,如果一个集合类型,没有实现 __contains__
方法,in
运算符就会按顺序做一次迭代搜索。
sorted 排序
In [20]: def sort_f(card):
...: rank_value = F.ranks.index(card.rank)
...: suit_dict = dict(spades=3,hearts=2,diamonds=1,clubs=0)
...: return rank_value * len(suit_dict) + suit_dict[card.suit]
...:
In [21]: sorted(f,key=sort_f)
虽然 FrenchDeck 隐式地继承了 object 类,5 但功能却不是继承而来的。我们通过数据模型和一些合成来实现这些功能。通过实现__len__
和__getitem__
这两个特殊方法,FrenchDeck 就跟一个 Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭代和切片)。同时这个类还可以用于标准库中诸如 random.choice、reversed 和 sorted 这些函数。另外,对合成的运用使得__len__
和__getitem__
的具体实现可以代理给 self._cards 这个 Python 列表(即 list 对象)。
特殊方法的存在是为了被python解释器调用的,不是我们自己来调用。比如,没有obj.__len()
这种写法,应该使用len(obj)
。是执行 len
方法,然后,调用了 __len__
方法。
对于python内置类型,__len__
会返回 PyVarObject 的 ob_size 属性,PyVarObject 是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值比调用一个方法要快的多。
for i in x:
对应的函数是 iter(x)
,调用的是 x.__iter__()
方法。
不要自己想当然地随意添加特殊方法,比如 foo 之类的,因为虽然现在这个名字没有被 Python 内部使用,以后就不一定了。
实现一个简单的向量类:
In [38]: class Vector:
...: def __init__(self,x,y):
...: self.x = x
...: self.y = y
...: def __abs__(self): # abs方法
...: return hypot(self.x,self.y)
...: def __add__(self,other): # + 运算符,中缀运算符
...: return Vector(self.x + other.x,self.y + other.y)
...: def __repr__(self): # repr方法,终端打印
...: return '({},{})'.format(self.x,self.y)
...: def __bool__(self): # bool方法
...: return bool(abs(self))
...: def __mul__(self,scalar): # * 运算符,中缀运算符
...: return Vector(self.x * scalar,self.y * scalar)
repr
方法
Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。
交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;
在老的使用% 符号的字符串格式中,这个函数返回的结果用来代替 %r 所代表的对象;同样,str.format 函数所用到的新式字符串格式化语法
__repr__
和 __str__
的区别:__str__
是 str()
函数使用时,被调用;或是在 print 函数调用
两个方法,__repr__
更好一点,因为如果一个对象没有 __str__
函数,而需要调用它时,解释器会用__repr__
作为替代而调用。
__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。
In [49]: a = Vector(0,0)
In [50]: a
Out[50]: (0,0)
In [51]: if a:
...: print('kkk')
...:
In [52]:
如果想让 Vector.__bool__
更高效,可以采用这种实现:
def __bool__(self):
return bool(self.x or self.y)
它不那么易读,却能省掉从 abs 到__abs__
到平方再到平方根这些中间步骤。通过bool 把返回类型显式转换为布尔值是为了符合__bool__
对返回值的规定,因为 or运算符可能会返回 x 或者 y 本身的值:若 x 的值等价于真,则 or 返回 x 的值;否则返回 y 的值。
为什么 len 不是普通方法:因为 对于内置类型 len方法 是直接读取属性,获取长度。
通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具 Python 风格的代码。
数据模型和对象模型;魔术方法与特殊方法;元对象协议=对象模型:构建核心语言的API