python最好的品质之一就是一致性,当使用python工作一会儿之后,就可以正确的猜出语句的意思。而这都归功于python的数据模型。
数据模型其实是对python框架的描述,它规范了这门语言自身 构建模块 的接口,这些模块包括序列、迭代器、函数、类和上下文管理器。
python解释器遇到特殊句法时,会使用特殊方法激活对象的基本操作,这些特殊方法的格式为:__xxx__
。比如obj[key]
背后的方法其实是调用了obj.__getitem__(key)
。
这种特殊方法,可以使我们自己的对象,实现和支持以下的语言框架,并与之交互:迭代、集合类、属性访问、运算符重载、函数和方法调用、对象创建和销毁、字符串表示形式和格式化、管理上下文。
接下来以实现一摞纸牌的例子,来展示如何实现__getitem__
和__len__
这两个特殊方法,见识到特殊方法的强大。
import collections
from random import choice
Card = namedTuple('Card',['rank', 'suit']) # 定义类型Card
class CardGenerator(object):
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 self.len(_card)
def __getitem__(self,pos):
return self._card[pos]
obj = CardGenerator()
print(len(obj)) # 获取长度
print(obj[0]) # 获取第一张牌
print(obj[-5:-3]) # 切片
print(choice(obj)) # 随机获取一张牌
print(obj[12::13]) # 先抽索引为12的那张牌,之后每13张抽一张牌
for card in reversed(obj): # 反向遍历
print(card)
首先,我们定义了一个有些像C结构体的类型Card,用来描述一张牌。然后,定义了CardGenerator类,用来描述一副纸牌。
我们都知道,自定义类型在没有重载运算符的情况下,不能够输入输出,不能够用标准库中的方法,更不可以迭代。而CardGenerator类,巧妙的运用了列表和定义特殊方法,只用了几行代码就使CardGenerator对象变成了一个,可获取长度的、可访问的、可迭代的、可以使用标准库方法的对象。
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def card_weight(card):
rank_values = CardGenerator.ranks.index(card.rank)
return rank_values * len(suit_values) + suit_values[card.suit]
for card in sorted(obj,key=card_weight):
print card
在定义了每张牌的权值之后,也可以通过调用sorted函数对其进行排序。
首先我们需要明确,特殊方法的存在是为了python解释器调用的,而不是我们自己调用。就比如上述代码中的len(obj)
,实际上是解释器调用了由我们自己实现的 CardGenerator类中的__len__()
特殊方法。
而对于python的内置类型如:list、str、bytearray等,len(list)
实际上不会调用__len__()
,而是会抄近路去调用,由C语言实现的PyVarObject结构体中的ob_size属性。直接调用这个属性会比调用函数快很多哦~
一般而言,许多特殊方法都是隐式调用的,比如for i in obj:
这个语句,其实隐式的调用了obj.__iter__()
特殊方法。并且大多数特殊方法是通过len、str这些内置函数调用的。
对于熟悉C++的各位,可以把python的特殊方法理解为虚函数。
我们通过实现一个二维向量类Vector来看一看数值类型是如何定义的。
from math import hypot # 引入取模函数
class Vector2(object):
def __init__(self, x, y):
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 __add__(self, other):
return Vector2(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector2(self.x * scalar, self.y * scalar)
def __bool__(self):
return bool(abs(self))
首先,值得一提的是,我们定义了__repr__()
方法,该方法返回一个对象的字符串以描述这个对象。在没有定义这个方法之前,我们在控制台输入print(Vector2(1, 2))
会得到对象的地址,而不是输出对象的内容。在定义了这个特殊方法之后,输入同样内容,在没有定义__str__()
的情况下,解释器会调用repr()
内置函数,而repr内置函数则会调用对象的__repr__()
方法。
之后,我们还定义了__bool__()
方法,当我们调用bool()
函数时,__bool__()
特殊方法便会被调用。对于一个新的数值类型,难免会在条件语句中进行判断,因此__bool__()
方法必不可少。当我们需要一个类型的bool值时,python解释器首先会看该类型内有没有__bool__()
方法,如果没有则会根据__len__()
方法的返回值确定bool值。
class test(object):
def __init__(self, val):
self.val = val
def __str__(self):
return 'test(%r)' % (self.val)
a = test(10)
a
>>> <__main__.a at 0x7fa91c314e50>
print(a)
>>> test(10)
class test(object):
def __init__(self, val):
self.val = val
def __repr__(self):
return 'test(%r)' % (self.val)
a = test(10)
a
>>> test(10)
print(a)
>>> test(10)
可以看出,对于__str__()
而言,输入对象名并不执行该函数,输出的依然是对象地址,输入print时才会执行。
当我们输入print(obj)
时,python解释器首先调用str()
内置函数,str()
调用__str__()
特殊方法。
如果当前类没有定义__str__()
特殊方法,那么解释器会调用repr()
内置函数,调用__repr__()
特殊方法。
可以看出,str是处于外层的,repr是处于底层的,若是定义了__str__()
,__repr__()
会被覆盖。因此,str一般面向用户,而repr面向程序员。