1.1 简介
数据模型其实是对Python框架得描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列,迭代器,函数,类和上下文管理器。
不管在那种框架下写程序,都会花费大量时间去实现哪些会被框架本身调用的方法,Python也不例外。Python解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开头,一两个下划线结尾(例如__getitem__)。比如obj[key]的背后就是__getitem__方法,为了能求得my_collection[key]的值,解释器实际上会调用my_collection.__getitem__(key)。
这些特殊方法名能让你自己的对象实现和支持一下语言架构,并与之交互:
~ 迭代
~ 集合类
~ 属性访问
~ 运算符重载
~ 函数和方法的调用
~ 对象的创建和销毁
~ 字符串表示形式和格式化
~ 管理上下文(即with块)
接下来我们使用一个非常简单的例子来展示如何实现__getitem__和__len__这两个特殊方法,通过这个例子我们也能见识到特殊方法的强大。
import collections
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._card = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
def __len__(self):
return len(self._card)
def __getitem__(self, position):
return self._card[position]
我们使用collections.namedtuple很容易得到一个对象:
beer_card = Card("7", "diamonds")
print(beer_card)
我们关注的重点应该是FrenchDeck类,它跟任何标准python集合类型一样,可以使用len()函数查看其长度
deck = FrenchDeck()
print(len(deck))
那么下面我们从中取出任意一个对象,比如最后一个和第一个,是很容易的,deck[0]或deck[-1],这都是由__getitem__方法提供的:
print(deck[-1])
print(deck[0])
接下来如果我们想要任意取出一个对象,还需要单独实现另外一个方法吗,并不用,因为python已经内置了一个方法randm.choice
现在是不是可以体会到通过实现特殊方法来利用python数据模型的两个好处:
①:作为你的类的用户,他们不必去记住标准操作的格式名称(怎么得到元素的总数?是.size()还是.lenght()还是其他什么)
②:可以更加方便的利用python的标准库,比如上面提到的random.choice函数,从而不用重复造轮子,因为__getitem__方法把[]操作交给了self._cards列表,所以我们的deck类自动支持切片操作。
总结:虽然FrenchDeck类隐性的继承了object类,但是功能却不是继承而来的。我们通过数据模型和一些合成来实现这些功能。通过实现__len__和__getitem__这两个特殊方法,FrenchDeck就跟一个python自由的序列数据模型一样,可以体现出python的核心语言特性(例如迭代和切片)。同时这个类还可以使用标准库。
1.2 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被python解释器调用的,你自己并不需要调用它们。也就是说没有obj.__len__()这种写法,而应该使用len(obj)。在执行len(obj)的时候,如果obj是一个自定义类的对象,那么python会自己去调用其中由你实现的__len__方法。
然而,如果是python内置的类型,例如list,str等,那么cpython会抄个近路,__len__实际上会直接返回pyVarObject里的ob_size属性。pyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值比调用一个方法快的多。
很多时候,特殊方法的调用是隐式的,比如for i in x:这个语句中,背后其实就调用了iter(x),而这个函数的背后则是x.__iter__()方法。当然前提是这个方法在x中被实现了。
通常你无须直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现他们的次数。唯一的例外可能是__init__方法,你可能经常会用到它,目的是在你自己的类中的__init__方法中调用超类的构造器。
字符串表示形式
python有一个内置的函数叫repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr就是通过__repr__这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现__repr__,当我们在控制台例打印一个实例时,得到的字符串可能会是<xx object at xxx>。
交互式控制台和调试程序(debugger)用repr函数来获取字符串表示形式;在老的使用%符号的字符串格式中,这个函数返回的结果用来代替%r所代表的对象;同样,str.format函数所用到的新式字符串格式化方法也是利用了repr,才把!r字段变成字符串。
__repr__和__str__的区别在于,后者是在str()函数被使用,或是在用print函数打印一个对象的时候才被调用,并且它返回的字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,__repr__是更好的选择,因为如果一个对象没有__str__函数,而python又需要调用它的时候,解释器会用__repr__作为代替。
1.3 特殊方法一览
python特殊方法
https://docs.python.org/3/reference/datamodel.html