Reproduce from
简介
什么是魔法方法呢?他们在面向对象的 Python 处处皆是。他们是一些可以让你对类添加「魔法」的特殊 function。他们经常是两个下划线包围来命名的(比如 __init__
,__lt__
)。
构造方法
最被熟知的基本魔法方法就是 init
,我们可以用它来指明一个对象初始化的行为。然而,当我们调用 x = SomeClass()
的时候,init
并不是第一个被调用的方法。事实上,第一个被调用的是 new
,这个方法才真正地创建了实例。当这个对象的生命周期结束的时候,del
会被调用。
- __new__(cls, [...])
__new__
是对象实例话时第一个调用的方法,它只取cls
参数,并把其他参数传给__init__
。__new__
很少使用,但是也有它适合的场景,尤其是当类继承自一个像元组或者字符串这样不经常改变的类型的时候。 - __init__(self, [...])
类的初始化方法。它获取任何传给构造器的参数(比如调用x = SomeClass(10, 'foo')
),__init__
就会接到参数10
和foo
。__init__
在 Python 的定义中用的最多。 - __del__(self)
__new__
和__init__
是对象的构造器。__del__
是对象的销毁器。它并非实现了语句del x
(因此该语句不等同于x.__del__()
)。而是定义了当对象被垃圾回收时的行为。当对象需要在销毁时做一些处理的时候,这个方法很有用,比如socket
对象,文件对象。但是需要注意的是,当 Python 解释器对出但对象仍然存活的时候,__del__
并不会执行。所以养成手工清理的好习惯是很重要的,比如即使关闭连接。
class FileObject(object):
'''文件对象的装饰类,用来保证文件被删除时能够正确关闭。'''
def __init__(self, file_path='~', file_name='sample.text'):
# 使用读写模式打开filepath中的filename文件
self.file = open(join(file_path, file_name), 'r+')
def __del__(self):
self.file.close()
def self.file
操作符
使用Python魔法方法的一个巨大优势就是可以构建一个拥有Python内置类型行为的对象。这意味着你可以避免使用非标准的、丑陋的方式来表达简单的操作。 在一些语言中,这样做很常见:
if instance.equals(other_instance):
# do something
你当然可以在 Python 里也这么做,但是这张做让代码变的冗长而混乱。不同的类库可能对同一种比较操作采用不同的方法名称,这让使用者需要很多没有必要的工作。运用魔法方法的魔力,可以定义方法 __eq__
。
if instance == other_instance:
# do something
比较操作符
Python 包含了一系列的魔法方法,用于实现对象之间直接比较,而不需要采用方法调用。同样也可以重载 Python 默认的比较方法,改变他们的行为。
- __cmp__(self, other)
__cmp__
是所有比较魔法方法中最基础的一个,它实际上定义了所有比较操作符的行为(<,==,!=,等等),但是他可能不能按照你需要的方式工作(例如,判断一个实例和另一个实例是否相等采用一套标准,而与判断一个实例是否大于另一实例采用另一套)。__cmp__
应该在self < other
是返回一个负整数,在self == other
时返回0,在self > other
时返回正整数。最好只定义你所需要的比较形式,而不一次定义全部。如果你需要实现所有的比较形式,而且他们的判断标准类似,那么__cmp__
是一个很好的方法,可以减少代码重复,让代码更简洁。 - __eq__(self, other)
定义等于操作符==
的行为。 - __ne__(self, other)
定于不等于操作符!=
的行为。 - __lt__(self, other)
定义小于操作符<
的行为。 - __gt__(self, other)
定义大于操作符>
的行为。 - __le__(self, other)
定义小于等于操作符<=
的行为。 - __ge__(self, other)
定义大于等于操作符>=
的行为。
假如我们用一个类来存储单词。我们可能想按照字典序(字母顺序)来比较单词,字符串的默认比较行为就是这样。我们可能也想按照其他规则来比较字符串,像是长度,或者音节的数量。在这个例子中,我们使用长度作为比较标准,下面是一种实现:
class Word(str):
'''单词类,按照单词长度来定义比较行为'''
def __new__(cls, word):
# 注意,我们只能使用 __new__,因为 str 是不可变类型
# 所以我们必须提前初始化它(在实例创建时)
if ' ' in word:
print("Value contains spaces. Truncating to first space.")
word = word[:word.index(' ')]
# Word现在包含第一个空格前的所有字母
return str.__new__(cls, word)
def __gt__(self, other):
return len(self) > len(other)
def __lt__(self, other):
return len(self) < len(other)
def __ge__(self, other):
return len(self) >= len(other)
def __le__(self, other):
return len(self) <= len(other)
现在我们可以创建两个 Word 对象(Word('foo') 和 Word('bar'))然后根据长度来比较他们。注意我们没有定义 __eq__
和 __ne__
,这是因为有时候他们会导致奇怪的结果(Word('foo') == Word('bar') 得到的结果会是 True
)。根据长度测试是否相等毫无意义,所以我们使用 str 的实现来比较相等。
不需要实现所有的比较方法,就可以使用丰富的比较操作。标准库还在 functools
模块中提供了一个类装饰器,只要我们定义__eq__
和 另一个操作符(__gt__
,__lt__
等),它就可以帮我们实现比较方法。这个特性只在 Python 2.7 中可用。当它可用时,它能帮助我们节省大量的时间和精力。要使用它,只需要它 @total_ordering 放在类的定义之上就可以了。
数值操作符
就像你可以使用比较操作符来比较类的实例,你也可以定义数值操作符的行为。这样的操作符真的很多。把它们分为五类:一元操作符,常见算数操作符,反射算数操作符(后面会涉及更多),增强赋值操作符,和类型转换操作符。
一元操作符
一元操作符只有一个操作符。
- __pos__(self)
实现取正操作,例如+some_object
- __neg__(self)
实现取负操作,例如-some_object
- __abs__(self)
实现内建绝对值函数abs()
操作 - __invert__(self)
实现取反操作符~
- __rount__(self, n)
实现内建函数round()
,n 是近似小数点的位数 - __floor__(self)
实现math.floor()
函数,即向下取整 - __ceil__(self)
实现math.ceil()
函数,即向上取整 - __trunc__(self)
实现 math.trunc() 函数,即距离零最近的整数
常见算数操作符
常见的二元操作符(和一些函数),像 +,-,* 之类的,它们很容易从字面意思理解。
- __add__(self, other)
实现加法操作 - __sub__(self, other)
实现减法操作 - __mul__(self, other)
实现乘法操作 - __floordiv__(self, other)
实现使用//
操作符的整数除法 - __div__(self, other)
实现使用/
操作符的除法 - __truediv__(self, other)
实现true
除法,这个函数只有使用from __future__ import division
时才有作用 - __mod__(self, other)
实现 % 取余操作符 - __divmod__(self, other)
实现 divmod 内建函数 - __pow__
实现 ** 操作符 - __lshift__(self, other)
实现左移位运算符 << - __rshift__(self, other)
实现右移位运算符 >> - __and__(self, other)
实现按位与运算符 & - __or__(self, other)
实现按位或运算符 | - __xor__(self, other)
实现按位异或运算符 ^
反射算数运算符
反射运算符,举个例子:
some_ohter + other
这是"常见"的加法,反射是一样的意思,只不过运算符交换了一下位置:
other + some_object
所有反射运算符魔法方法和它们的常见版本做的工作相同,只不过是处理交换两个操作数之后的情况。绝大多数情况下,反射运算和正常顺序产生的结果是相同的,所以很可能定义 radd
时只是调用一下 add
。注意,操作符左侧的对象(也就是上面的 other
)一定不要定义(或产生 NotImplemented
异常)操作符的非反射版本。例如,在上面的例子中,只有当 other
没有定义 add
时 some_other.radd
才会被调用。
- __radd__(self, other)
实现反射加法操作 - __rsub__(self, other)
实现反射减法操作 - __rmul__(self, other)
实现反射乘法操作 - __rfloordiv__(self, other)
实现使用//
操作符的整数反射除法 - __rdiv__(self, other)
实现使用/
操作符的反射除法 - __truediv__(self, other)
实现true
反射除法,这个函数只有使用from __future__ import division
时才有作用 - __rmod__(self, other)
实现 % 反射取余操作符 - __rdivmod__(self, other)
实现调用divmod(other, self)
时divmod
内建函数的操作。 - __rpow__
实现 ** 反射操作符 - __rlshift__(self, other)
实现反射左移位运算符 << - __rrshift__(self, other)
实现反射右移位运算符 >> - __rand__(self, other)
实现反射按位与运算符 & - __ror__(self, other)
实现反射按位或运算符 | - __rxor__(self, other)
实现反射按位异或运算符 ^
增强赋值运算符
Python 提供了大量的魔术方法,可以用来自定义增强赋值操作的行为。它融合了 "常见" 的操作符和复制操作,例子:
x = 5
x += 1 # 也就是 x = x + 1
这些方法都应该返回左侧操作数应该被赋予的值(例如,a += b __iadd__
也许会返回 a + b,这个结果会赋给 a),下面是方法列表:
- __iadd__(self, other)
实现加法复制操作 - __isub__(self, other)
实现减法赋值操作 - __imul__(self, other)
实现乘法赋值操作 - __ifloordiv__(self, other)
实现使用//=
操作符的整数除法赋值操作 - __idiv__(self, other)
实现使用/=
操作符的除法赋值操作 - __iturediv__(self, other)
实现 true 除法赋值操作,这个函数只有使用from __future__ import division
时才有作用 - __imod__(self, other)
实现实现%=
取余赋值操作 - __ipow__
实现**=
操作 - __ilshift__(self, other)
实现左移位赋值运算符<<=
- __irshift__(self, other)
实现右移位赋值运算符>>=
- __iand__(self, other)
实现按位与运算符&=
- __ior__(self, other)
实现按位异或赋值运算符|
- __ixor__(self, other)
实现按位异或赋值运算符^=
类型转换运算符
Python 也有一系列的魔法方法用于实现类似 float()
的内建类型转换函数的操作:
- __int__(self)
实现到 int 类型的转换 - __long__(self)
实现到 long 的类型转换 - __float__(self)
实现到 float 的类型转换 - __complex__(self)
实现到 complex 的类型转换
... - __index__(self)
实现当对象用于切片表示时到一个整数的类型转换。如果你定义了一个可能会用于切片操作的数值类型,你应该定义__index__
- __trunc__(self)
当调用math.trunc(self)
时调用该方法,__trunc__
应该返回self
截取到一个整数类型(通常是 long 类型)的值 - __coerce__(self)
该方法用于实现混合模式算术运算,如果不能进行类型转换,__coerce__
应该返回 None。反之,它应该返回一个二元组self
和other
,这两者均已被转换成相同的类型
类的表示
使用字符串来表示类是一个相当有用的特性。在 Python 中有一些内建方法可以返回类的表示,相对应的,也有一系列魔法方法可以用来自定义在使用这些内建函数式类的行为。
- __str__(self)
定义对类的实例调用str()
时的行为 - __repr__(self)
定义对类的实例调用repr()
时的行为。str()
和repr()
最主要的差别在于 “目标用户”。repr()
的作用是产生机器可读的输出(在大部分情况下,起输出可以作为有效的 Python 代码),而str()
侧产生人类可读的输出。 - __unicode__(self)
定义对类的实例调用unicode()
时的行为。unicode()
和str()
很像,只是它返回 unicode 字符串。注意,如果调用者试图调用str()
而你的类只实现了__unicode__
字符串,那么类将不能正常工作。所以你应该总定义__str__()
,以防有些人没有使用unicode()
。 - __format__(self)
定义当类的实例用于新式字符串格式化时的行为,例如,"Hello,{}:abc!".format("abc")
会导致调用a.__format__("abc")
。当定义你自己的数值类型或字符串类型时,你可能想提供某些特殊的格式化选项,这种情况下这个魔法方法会非常有用。 - __hash__(self)
定义对类的实例调用hash()
时的行为。它必须返回一个整数,其结果会被用于字典中键的快速比较。同时注意一点,实现这个魔法方法通常也需要实现__eq__
,并且遵守如下的规则:a == b
意味着hash(a) == hash(b)
。 - __nonzero__(self)
定义对类的实例调用bool()
时的行为,根据你自己对类的设计,针对不同的实例,这个魔法方法应该相应地返回True
或False
。 - __dir__(self)
定义对类的实例调用 dir() 时的行为,这个方法应该像调用者返回一个属性列表。一般来说,没必要自己实现__dir__
。但是如果你重定义了__getattr__
或者__getattribute__
,乃至使用动态生成的属性,以实现类的交互式使用,那么这个魔法方法是必不可少的。
访问控制
很多从其他语言转向 Python 的人抱怨 Python 的类缺少真正意义上的封装(即没办法定义私有属性然后使用共有的 getter 和 setter)。然而事实并非如此。实际上 Python 不是通过现实定义的字段和方法修改器,而是通过魔法方法实现了一系列的封装。
- __getattr__(self, name)
当用户试图访问一个根本不存在的(或者暂时不存在)属性时,你可以通过这个魔法方法来定义类的行为。这个可以用于捕捉错误的拼写并且给出指引,使用废弃属性时给出警告(如果你愿意,仍然可以计算并且返回该属性),以及灵活地处理 AttributeError。只有当试图访问不存在的属性时它才会被调用,所以这不能算是一个真正的封装的办法。 - __setattr__(self, name, value)
和__getattr__
不同,__setattr__
可以用于真正意义上的封装。它允许你定义某个属性的赋值行为,不管这个属性存在与否,也就是说你可以对任意属性的任何变化都定义自己的规则。然后,一定要小心使用__setattr__
! - __delattr__(self, name)
这个魔法方法和__setattr__
几乎相同,只不过它是用于处理删除属性时的行为。和__setattr__
一样,使用它时也需要多加小心,防止产生无限递归(在__delattr__
的实现中调用 del self.name 会导致无限递归)。 - __getattribute__(self, name)
__getattribute__
看起来和上面那些方法很合得来,但是最好不要使用它。__getattribute__
只能用新式类。在最新版的 Python 中所有的类都是新式类。在最新版的 Python 中所有的类都是新式类,在老版 Python 中可以使用继承object
来创建新式类。__getattribute__
允许你自定义属性被访问时的行为,它也同样可能遇到无限递归问题(通过调用基类的__getattribute__
来避免)。__getattribute__
基本上可以替代__getattr__
。只有当它被实现,并且显示地被调用,或者产生AttributeError
时它才被使用。这个魔法方法可以被使用,不过不推荐使用它,因为它的使用范围相对有限(通常我们想要在赋值时进行特殊操作,而不是取值时),而且实现这个方法很容易出现 Bug。
自定义这些控制属性访问的魔法方法很容易导致问题,考虑下面这个例子:
def __setattr__(self, name, value):
self.name = value
# 因为每次属性赋值都要调用 __setattr__(),所以这里的实现会导致递归
# 这里的调用实际上是 self.__setattr('name', value)。因为这个方法一直
# 在调用自己,因此递归将持续进行,知道程序崩溃
def __setattr__(self, name, value):
self.__dict__[name] = value # 使用 __dict__ 进行赋值
# 自定义行为
再次重申,Python 的魔法方法十分强大,能力越强责任越大,了解如何正确的使用魔法方法更加重要。
到这里,我们对 Python 的自定义属性存取控制有了什么样的印象?它并不适合轻度的使用。实际上,它有些过份强大,而且违反直觉。然而它之所以存在,是因为一个更大的原则:** Python 不指望杜绝坏事发生,而是想办法让做坏事变得困难。**自由是至高无上的权利,你真的可以随心所欲。下面的例子展示了实际应用中某些特殊的属性访问方法(注意我们之所以使用 super 是因为不是所有的类都 __dict__
属性):
class AccessCounter(object):
''' 一个包含了一个只并且实现了访问计数器的类
每次值的变化都会导致计数器自增'''
def __init__(self, val):
super(AccessCounter, self).__setattr__('counter', 0)
super(AccessCounter, self).__setattr__('value', val)
def __setattr__(self, name, value):
if name == 'value':
super(AccessCounter, self).__setattr__('counter', self.counter + 1)
# 使计数器自增变成不可避免
# 如果你想阻止其他属性的赋值行为
# 产生 AttributeError(name) 就可以了
super(AccessCounter, self).__setattr__(name, value)
def __delattr__(self, name):
if name == 'value':
super(AccessCounter, self).__setattr__('counter', self.counter + 1)
super(AccessCounter, self).__delattr(name)
自定义序列 Making Custom Sequences
有许多魔法办法可以让你的 Python 类表现得像是内建序列类型(字典,元组,列表,字符串等)。他们给了你难以置信的控制能力,可以让你的类与一系列的全局函数完美结合。
预备知识 Requirements
既然讲到创建自己的序列类型,就不得不说协议 protocols。协议类似某些语言中的接口,里面包含的是一些必须实现的方法。在 Python 中,协议完全是非正式的(totally informal),也不需要显式的声明,实际上,他们更像是一种参考标准 guildlines。
在 Python 的实现自定义容器类型需要用到一些协议。首先,不可变容器类型有如下协议:想实现一个不可变容器,你需要定义 __len__
和 __getitem__
。可变容器的协议除了上面的两个方法外,还需要定义 __setitem__
和 __delitem__
。最后,如果想让对象可以迭代,需要定义 __iter__
,这个方法返回一个迭代器。迭代器必须遵守迭代器协议,需要定义 __iter__
(返回它自己)和 next
方法。
容器背后的魔法方法
- __len__(self)
返回容器的长度,可变和不可变类型都需要实现。 - __getitem__(self, key)
定义对容器中某一项使用self[key]
的方式进行读取操作时的行为。这也是可变和不可变容器类型都需要实现的一个方法。它应该在键的类型错误时产生 TypeError 异常,同时在没有键值相匹配的内容是产生 KeyError 异常。 - __setitem__(self, key)
定义对容器中某一项使用self[key]
的方式进行赋值操作时的行为。它是可变容器类型必须实现的一个方法,童颜应该在合适的时候产生 KeyError 和 TypeError 异常。 - __iter__(self, key)
它应该返回当前容器的一个迭代器。迭代器以一连串内容的形式返回,最常见的是使用iter()
函数调用,以及在类似for x in container
的循环中被调用。迭代器是他们自己的队形,需要定义__iter__
方法并在其中返回自己。 - __reversed__(self)
定义了对容器使用reversed()
内建函数时的行为。它应该返回一个反转之后的序列。当你的序列类是有序时,类似列表和元组,再实现这个方法。 - __contains__(self, item)
__contains__
定义了使用in
和not in
进行成员测试时类的行为。这个方法不是序列协议的一部分,原因是,如果__contains__
没有定义,Python 就会迭代整个序列,如果找到了需要的一项就返回 Ture。 - __missing__(self, key)
__missing__
在字典的子类中使用,他定义了当试图方位一个字典中不存在的键时的行为(目前为止是指字典的实例,例如我有一个字典 d , 'george' 不是字典中的一个键,当试图访问d['george']
时就会调用d.__missing__('george')
)。
例子
一个实现了一些函数式结构的列表
class FunctionalList:
''' 一个列表的封装类,实现了一些额外的函数式方法
例如 head, tail, init, last, drop 和 take
'''
def __init__(self, values=None):
if values if None:
self.values = []
else:
self.values = values
def __len__(self):
return len(self.values)
def __getitem__(self, key):
# 如果键的类型或值不合法,列表会返回异常
return self.values[key]
def __setitem__(self, key, value):
self.values[key] = value
def __delitem__(self, key):
del self.values[key]
def __iter__(self):
return iter(self.values)
def __reversed__(self):
return reversed(self.values)
def append(self, value):
self.values.append(value)
def head(self):
# 取得第一个元素
return self.values[0]
def tail(self):
# 取得除第一个元素外的所有元素
return self.values[1:]
def init(self):
# 取得除最后一个元素外的所有元素
return self.values[:-1]
def last(self):
# 取得最后一个元素
return self.values[-1]
def drop(self, n):
# 取得除前n个元素外的所有元素
return self.values[n:]
def take(self, n):
# 取得前n个元素
return self.values[:n]
当然,自定义序列有更大的用处,而且绝大部分都在标准库中实现了(Python 都是自带库的),像 Counter, OrderedDict 和 NamedTuple。
反射 Reflection
可以通过定义魔法方法来控制用于反射的内建函数 isinstance
和 issubclass
的行为。
- __instancecheck__(self, instance)
检查一个实例是否是你定义的类的一个实例(例如isinstance(instance, class)
)。 - __subclasscheck__(self, subclass)
检查一个类是否是你定义的类的子类(例如insubclass(subclass, class)
)。
这几个魔法方法的使用范围有些窄。相比其他魔法方法他们显得不是很重要。但是他们展示了在 Python 中进行面向对象编程时很重要的一点:不管做什么事情,都会有一个简单方法,不管它用不常用。这些魔法方法可能看起来没那么有用,但是当你正正需要用到他们的时候,你会感到幸运,因为他们还在那儿。
抽象基类
请参考 http://docs.python.org/2/library/abc.html
可调合的对象
在 Python 中,函数时一等的对象 (functions are first-class objects)。这意味着他们可以像其他任何对象一样被传递到函数和方法中,这是一个十分强大的特性。
Python 中一个特殊的魔法方法允许你自己类的对象表现得像是函数,然后你就可以“调用”他们,把它们传递到使用函数做参数的函数中,等等。这是另一个强大而且方便的特性,让使用 Python 编程变得更加幸福。
-
__call__(self, [args...])
允许类的一个实例像函数那样被调用。本质上代表了x()
和x.__call__()
是相同。注意,__call__
可以有多个参数,这代表你可以像定义其他任何函数一样,定义__call__
,喜欢用多少参数就用多少。__call__
在某些需要经常改变状态的类的实例中显得特别有用。“调用”这个实例来改变他的状态,是一种更加符合直觉,也更加优雅的方法。一个表示平面上实体的类是一个不错的例子:
class Entity:
''' 表示一个实体的类,调用它的实例可以更新实体的位置 entity's position'''
def __init__(self, size, x, y):
self.x, self.y = x, y
self.size = size
def __call__(self, x, y):
'''改变实体的位置'''
self.x, self.y = x, y
上下文管理器 Context Manager
在 Python 2.5 中引入了一个全新的关键词,随之而来的是一种新的代码复用方法 -- with 声明。上下文管理的概念在 Python 中并不是全新引入的(之前他作为标准库的一部分实现),直到 PEP 343 被接受,它才成为一种一级的语言结构。可能你已经见过这种写法了:
with open('foo.txt') as bar:
# 使用 bar 进行某些操作
当对象使用 with
声明创建时,上下文管理器允许类做一些设置和清理工作。上下文管理器的行为有下面两个魔方方法所定义:
- __enter__(self)
定义使用with
声明创建的语句块最开始上下文管理器应该做些什么。注意__enter__
的返回值会赋给with
声明的目标,也就是as
之后的东西。 - __exit__(self, exception_type, exception_value, traceback)
定义当 with 声明语句块执行完毕(或终止)时上下文管理器的行为。它可以用来处理异常,进行清理,或者做其他应该在语句块结束之后立刻执行的工作。如果语句块顺利执行,exception_type
,exception_value
和traceback
会是None
。否则,你可以选择处理这个异常或者让用户来处理。如果你想处理异常,确保__exit__
在完成工作之后返回True
。如果你不想处理异常,那就让它发生吧。
对一些具有良好定义的且通用的设置和清理行为的类,__enter__
和 __exit__
会变得特别有用。你也可以使用这几个方法来创建通用的上下文管理器,用来包装其他对象。下面是一个例子:
class Closer():
"""一个上下文管理器,可以在 with 语句中
使用 close() 自动关闭对象"""
def __init__(self, obj):
self.obj = obj
def __enter__(self, obj):
return self.obj # 绑定到目标
def __exit__(self, exception_type, exception_value, traceback):
try:
self.obj.close()
except AttributeError: # obj 不是可关闭的
print 'Not cloaseable.'
return True
这是一个 Closer 在实际使用中的例子,使用一个 FTP 连接演示(一个可关闭的 socket):
>>> from magicmethods import Closer
>>> from ftplib import FTP
>>> with Closer(FTP('ftp.somesite.com')) as conn:
... conn.dir()
...
# 为了简单,省略了某些输出
>>> conn.dir()
... # 很长的 AttributeError 信息,不能使用一个已关闭的连接
>>> with Closer(int(5)) as i:
... i += 1
...
Not closable.
>>> i
6
看到我们的包装器是如何同时优雅地处理正确和不正确的调用了吗?这就是上下文管理器和魔法方法的力量。Python 标准库包含一个 contextlib
模块,里面有一个上下文管理器 contextlib.closing()
基本上和我们的包装器完成的是同样的事情(但是没有包含任何当对象没有 close()
方法时的处理)。
创建描述符对象 Building Descriptor Objects
描述符 Descriptor 是一个类,当使用取值,赋值,和删除时它可以改变其它对象。描述符不是用来单独使用的,它们需要被一个拥有着类所包含(they're meant to be held by an owner class)。描述符可以用来创建面向对象数据库,以及创建某些属性之间互相依赖的类。描述符在表现具有不同单位的属性,或者需要计算的属性时显得特别有用(例如表现一个坐标系中的点的类,其中的距离原点的距离这种属性)。
要想成为一个描述符,一个类必须具有实现 __get__
, __set__
,和 __delete__
三个方法中至少一个。
- __get__(self, instance, owner)
定义当试图取出描述符的值时的行为。instance
是拥有者类的实例,owner
是拥有者类本身。 - __set__(self, instance, value)
定义当描述符的值改变时的行为。instance 是拥有者类的实例,value 是要付给描述符的值。 - __delete__(self, instance, owner)
定义当描述符得知被删除时的行为。instance 是拥有者类的实例。
看一个描述符的有效应用,单位转换:
class Meter(object):
''' 米的描述符'''
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Foot(object):
"""英尺的描述符"""
def __get__(self, instance, owner):
return instance.meter * 3.2808
def __set__(self, instance, value):
instance.meter = float(value) / 3.2808
class Distance(object):
"""用于描述距离的类,包含英尺和米两个描述符"""
meter = Meter()
foot = Foot()
>>> dis = Distance()
>>> dis.meter = 10
>>> dis.meter
10.0
>>> dis.foot
32.808
拷贝
当拷贝一个对象,改变这个对象而不影响原有的对象。这时就需要用到 Python 的 copy 模块了。Python 模块并不具有感知能力,我们需要告诉 Python 如何有效率的拷贝对象。
- __copy__(self)
定义对类的实例使用copy.copy()
时的行为。copy.copy()
返回一个对象的浅拷贝,这意味着拷贝出的实例是全新的,然而里面的数据全都是引用的。也就是说,对象本身是拷贝的,但是它的数据还是引用的(所以浅拷贝中的数据更改会影响原对象)。 - __deepcopy__(self, memodict)
定义对类的实例使用copy.deepcopy()
时的行为。copy.deepcopy()
返回一个对象的深拷贝,这个对象和他的数据全都被拷贝了一份。memodict
是一个先前拷贝对象的缓存,它优化了拷贝过程,而且可以防止拷贝递归数据结构是产生无限递归。当你想深拷贝一个单独的属性时,在那个属性上调用copy.deepcopy()
,使用memodict
作为第一个参数。
这些魔法方法有什么用呢?像往常一样,当你需要比默认行为更加精确的控制时。
Pickling
Pickling 是 Python 数据结构的序列化过程,当你想存储一个对象稍后再取出读取时,Pickling 会显得十分有用。然而它同样也是担忧和混淆的主要来源。
Pickling 是如此的重要,以至于它不仅仅有自己的模块(pickle),还有自己的协议和魔法方法。首先,我们先来简要的介绍一下如何 pickle 已存在的对象类型。
Pickling:A Quick Soak in the Brine
假设你有一个字典,你想存储它,稍后再取出来。你可以把它的内容写入一个文件,小心翼翼地确保使用了正确地格式,要把它读取出来,你可以使用 exec()
或处理文件输入。但是这种方法并不可靠:如果你使用纯文本来存储重要数据,数据很容易以多种方式被破坏或者修改,导致你的程序崩溃,更糟糕的情况下,还可能在你的计算机上运行恶意代码。因此,我们要 pickle 它:
import pickle
data = {
'foo': [1, 2, 3],
'bar': ('Hello', 'world'),
'baz': True
}
jar = open('data.pkl', 'wb')
pickle.dump(data. jar) # 将 pickle 后的数据写入 jar 文件
jar.close()
过了几个小时,我们想把它取出来,我们只需要反 pickle 它:
import pickle
pkl_file = open('data.pkl', 'rb') # 与 pickle 后的数据连接
data = pickle.load(pkl_file) # 把它加载进一个变量
print data
pkl_file.close()
它就是我们之前的 data 。
现在,还需要谨慎地说一句: pickle 并不完美。Pickle 文件很容易因为事故或被故意的破坏掉。Pickling 或许比纯文本文件安全一些,但是依然有可能被用来运行恶意代码。而且它还不支持跨 Python 版本,所以不要指望分发pickle对象之后所有人都能正确地读取。然而不管怎么样,它依然是一个强有力的工具,可以用于缓存和其他类型的持久化工作。
Pickle 你的对象
Pickle 不仅仅可以用于内建类型,任何遵守 pickle 协议的类都可以被 pickle。Pickle 协议有四个可选方法,可以让类自定义他们的行为。
- __getinitargs__(self)
如果你想让你的类在反 pickle 时调用__init__
,你可以定义__getinitargs__(self)
,它会返回一个参数元组,这个元组会传递给__init__
。注意,这个方法只能用于旧式类。 - __getnewargs__(self)
对新式类来说,可以通过这个方法改变类在 pickle 时传递给__new__
的参数。这个方法应该返回一个参数元组。 - __getstate__(self)
可以自定义对象被 pickle 时被存储的状态,而不是用对象的__dict__
属性。这个状态在对象被反 pickle 时会被__setstate__
使用。 - __setstate__(self)
当一个对象被反 pickle 时,如果定义了__setstate__
,对象的状态会传递给这个魔法方法,而不是直接应用到对象的__dict__
属性。这个魔法方法和__get__
相互依存:当这两个方法都被定义时,你可以在 pickle 时使用任何方法保存对象的任何状态。 - __reduce__(self)
当定义扩展类型时(也就是使用 Python 的 C 语言 API 实现的类型),如果你想 pickle 它们,你必须告诉 Python 如何 pickle 它们。__reduce__
被定义之后,当对象被 pickle 时就会被调用。他要么返回一个代表全局名称的字符串,Python 会查找它并 pickle,要么返回一个元组。这个元组包含 2 到 5 个元素,其中包括一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给__setstate__
的状态(可选);一个产生被 pickle 的列表元素的迭代器(可选);一个产生被 pickle 的字典元素的迭代器(可选); - __reduce_ex__(self)
__reduce_ex__
的存在是为了兼容性。如果它被定义,在 pickle 时__reduce_ex__
会代替__reduce__
被调用。__reduce__
也可以被定义,用于不支持__reduce_ex__
的旧版 pickle 的 API 调用。
一个例子
例子是 Slate,它会记住它的值曾经是什么,以及那些值是什么时候赋给他的。然而每次被 pickle 时它都会变成空白,因为当前的值不会被存储:
import time
class Slate:
'''存储一个字符串和一个变更日志的类
每次被 pickle 都会忘记它当前的值
'''
def __init__(self, value):
self.value = value
self.last_change = time.asctime()
self.history = {}
def change(self, new_value):
# 改变当前值,将上一个值就到历史
self.history[self.last_change] = self.value
self.value = new_value
self.last_change = time.asctime()
def print_change(self):
print('Change log for Slate object:')
for k, v in self.history.items():
print('{}\t{}'.format()k, v)
def __getstate__(self):
# 故意不返回 self.value 或 self.last_change
# 我们想在反 pickle 时得到一个空白的 slate
return self.history
def __setstate__(self):
# 使self.history = slate
# value 和 last_change 为定义
self.history = state
self.value, self.last_change = None, None
总结
这本指南的目标是使所有阅读它的人都能有所收获,无论他们有没有使用 Python 或者进行面向对象编程的经验。如果你刚刚开始学习Python,你会得到宝贵的基础知识,了解如何写出具有丰富特性的,优雅而且易用的类。如果你是中级的 Python 程序员,你或许能掌握一些新的概念和技巧,以及一些可以减少代码行数的好办法。如果你是专家级别的 Python 爱好者,你又重新复习了一遍某些可能已经忘掉的知识,也可能顺便了解了一些新技巧。无论你的水平怎样,我希望这趟遨游 Python 特殊方法的旅行,真的对你产生了魔法般的效果(实在忍不住不说最后这个双关)。
附录1:如何调用魔法方法
一些魔法方法直接和内建函数对应,这种情况下,如何调用它们是显而易见的。然而,另外的情况下,调用魔法方法的途径并不是那么明显。这个附录旨在展示那些不那么明显的调用魔法方法的语法。
魔法方法 | 什么时候被调用 | 解释 |
---|---|---|
__new__(cls [,...]) |
instance = MaClass(arg1, arg2) |
__new__ 在实例创建时调用 |
__init__(self[,...]) |
instance = MyClass(arg1, arg2) |
__init__ 在实例创建时调用 |
__cmp__(self) |
self == other , self > other 等 |
进行比较时调用 |
__pos__(self) |
+self |
一元加法符号 |
__neg__(self) |
-self |
一元减法符号 |
__invert__(self) |
~self |
按位取反 |
__index__(self) |
x[self] |
当对象用于索引时 |
__nonzero__(self) |
bool(self |
对象的布尔值 |
__getattr__(self, name) |
self.name # name 不存在 |
访问不存在的属性 |
__setattr__(self, name) |
self.name = val |
给属性赋值 |
__delattr__(self, name) |
def self.name |
删除属性 |
__getattribute(self, name) |
self.name |
访问任意属性 |
__getitem__(self, key) |
self[key] |
使用索引给某个元素赋值 |
__setitem__(self, key) |
self[key] = val |
使用索引给某个元素赋值 |
__delitem__(self, key) |
del self[key] |
使用索引删除某个对象 |
iter(self) |
for x in self |
迭代 |
__contains__(self, value) |
value in self, value not in self |
使用 in 进行成员测试 |
__call__(self [,...] |
self(args) |
“调用”一个实例 |
__enter__(self) |
with self as x: |
with 声明的上下文管理器 |
__exit__(self, exc, val, trace) |
with self as x |
with 声明的上下文管理器 |
__getstate__(self) |
pickle.dump(pkl_file, self) |
Pickling |
__setstate(self) |
data = pickle.load(pkl_file) |
Pickling |
Python 3 中的变化
记录几个在对象模型方面 Python 3 和 Python 2.x 之间的主要区别。
Python 3 中 string 和 unicode 的区别不复存在,因此 __unicode__
被取消了, __bytes__
加入进来(与 Python 2.7 中的 __str__
和 __unicode__
行为类似),用于新的创建字节数组的内建方法。
Python 3 中默认除法变成了 true 除法,因此 __div__
被取消了。
__coerce__
被取消了,因为和其他魔法方法有功能上的重复,以及本身行为令人迷惑。
__cmp__
被取消了,因为和其他魔法方法有功能上的重复。
__nonzero__
被重命名成 __bool__
。