前言
在python中,所有以”__”双下划线包起来的方法,都统称为”魔术方法”。比如我们接触最多的__init__.
有些魔术方法,我们可能以后一辈子都不会再遇到了,这里也就只是简单介绍下;
而有些魔术方法,巧妙使用它可以构造出非常优美的代码,比如将复杂的逻辑封装成简单的api。
本文编辑的思路借鉴自rafe kettler的这篇博客: a guide to python magic methods,并补充了一些代码示例。
介绍的顺序大概是:常见的先介绍,越少见的越靠后讲。
本文中用到的代码示例,可以在我的github上下载到。
构造和初始化
__init__我们很熟悉了,它在对象初始化的时候调用,我们一般将它理解为”构造函数”.
实际上, 当我们调用x = someclass()的时候调用,__init__并不是第一个执行的, __new__才是。所以准确来说,是__new__和__init__共同构成了”构造函数”.
__new__是用来创建类并返回这个类的实例, 而__init__只是将传入的参数来初始化该实例.
__new__在创建一个实例的过程中必定会被调用,但__init__就不一定,比如通过pickle.load的方式反序列化一个实例时就不会调用__init__。
__new__方法总是需要返回该类的一个实例,而__init__不能返回除了none的任何值。比如下面例子:
class foo(object):
def __init__(self):
print ‘foo __init__’
return none # 必须返回none,否则抛typeerror
def __del__(self):
print ‘foo __del__’
实际中,你很少会用到__new__,除非你希望能够控制类的创建。如果要讲解__new__,往往需要牵扯到metaclass(元类)的介绍。如果你有兴趣深入,可以参考我的另一篇博客: 理解python的metaclass
对于__new__的重载,python文档中也有了详细的介绍。
在对象的生命周期结束时, __del__会被调用,可以将__del__理解为”构析函数”.__del__定义的是当一个对象进行垃圾回收时候的行为。
有一点容易被人误解, 实际上,x.__del__() 并不是对于del x的实现,但是往往执行del x时会调用x.__del__().
怎么来理解这句话呢? 继续用上面的foo类的代码为例:
foo = foo()
foo.__del__()
print foo
del foo
print foo # nameerror, foo is not defined
如果调用了foo.__del__(),对象本身仍然存在. 但是调用了del foo, 就再也没有foo这个对象了.
请注意,如果解释器退出的时候对象还存在,就不能保证 __del__ 被确切的执行了。所以__del__并不能替代良好的编程习惯。比如,在处理socket时,及时关闭结束的连接。
属性访问控制
总有人要吐槽python缺少对于类的封装,比如希望python能够定义私有属性,然后提供公共可访问的getter和 setter。python其实可以通过魔术方法来实现封装。
__getattr__(self, name)
该方法定义了你试图访问一个不存在的属性时的行为。因此,重载该方法可以实现捕获错误拼写然后进行重定向, 或者对一些废弃的属性进行警告。
__setattr__(self, name, value)
__setattr__ 是实现封装的解决方案,它定义了你对属性进行赋值和修改操作时的行为。不管对象的某个属性是否存在,它都允许你为该属性进行赋值,因此你可以为属性的值进行自定义操作。有一点需要注意,实现__setattr__时要避免”无限递归”的错误,下面的代码示例中会提到。
__delattr__(self, name)
__delattr__与__setattr__很像,只是它定义的是你删除属性时的行为。实现__delattr__是同时要避免”无限递归”的错误。
__getattribute__(self, name)
__getattribute__定义了你的属性被访问时的行为,相比较,__getattr__只有该属性不存在时才会起作用。
因此,在支持__getattribute__的python版本,调用__getattr__前必定会用 __getattribute__。__getattribute__同样要避免”无限递归”的错误。
需要提醒的是,最好不要尝试去实现__getattribute__,因为很少见到这种做法,而且很容易出bug。
例子说明__setattr__的无限递归错误:
def __setattr__(self, name, value):
self.name = value
# 每一次属性赋值时, __setattr__都会被调用,因此不断调用自身导致无限递归了。
因此正确的写法应该是:
def __setattr__(self, name, value):
self.__dict__[name] = value
__delattr__如果在其实现中出现del self.name 这样的代码也会出现”无限递归”错误,这是一样的原因。
下面的例子很好的说明了上面介绍的4个魔术方法的调用情况:
class access(object):
def __getattr__(self, name):
print ‘__getattr__’
return super(access, self).__getattr__(name)
def __setattr__(self, name, value):
print ‘__setattr__’
return super(access, self).__setattr__(name, value)
def __delattr__(self, name):
print ‘__delattr__’
return super(access, self).__delattr__(name)
def __getattribute__(self, name):
print ‘__getattribute__’
return super(access, self).__getattribute__(name)
access = access()
access.attr1 = true # __setattr__调用
access.attr1 # 属性存在,只有__getattribute__调用
try:
access.attr2 # 属性不存在, 先调用__getattribute__, 后调用__getattr__
except attributeerror:
pass
del access.attr1 # __delattr__调用描述器对象
我们从一个例子来入手,介绍什么是描述符,并介绍__get__, __set__, __delete__ 的使用。(放在这里介绍是为了跟上一小节介绍的魔术方法作对比)
我们知道,距离既可以用单位”米”表示,也可以用单位”英尺”表示。现在我们定义一个类来表示距离,它有两个属性: 米和英尺。
class meter(object):
”’descriptor for a meter.”’
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):
”’descriptor for a foot.”’
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()
d = distance()
print d.meter, d.foot # 0.0, 0.0
d.meter = 1
print d.meter, d.foot # 1.0 3.2808
d.meter = 2
print d.meter, d.foot # 2.0 6.5616
在上面例子中,在还没有对distance的实例赋值前, 我们认为meter和foot应该是各自类的实例对象, 但是输出却是数值。这是因为__get__发挥了作用.
我们只是修改了meter,并且将其赋值成为int,但foot也修改了。这是__set__发挥了作用.
描述器对象(meter、foot)不能独立存在, 它需要被另一个所有者类(distance)所持有。
描述器对象可以访问到其拥有者实例的属性,比如例子中foot的instance.meter。
在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。在django的orm中, models.model中的interfield等字段, 就是通过描述器来实现功能的。
一个类要成为描述器,必须实现__get__, __set__, __delete__ 中的至少一个方法。下面简单介绍下:
__get__(self, instance, owner)
参数instance是拥有者类的实例。参数owner是拥有者类本身。__get__在其拥有者对其读值的时候调用。
__set__(self, instance, value)
__set__在其拥有者对其进行修改值的时候调用。
__delete__(self, instance)
__delete__在其拥有者对其进行删除的时候调用。
构造自定义容器(container)
在python中,常见的容器类型有: dict, tuple, list, string。
其中tuple, string是不可变容器,dict, list是可变容器。
可变容器和不可变容器的区别在于,不可变容器一旦赋值后,不可对其中的某个元素进行修改。
比如定义了l = [1, 2, 3]和t = (1, 2, 3)后, 执行l[0] = 0是可以的,但执行t[0] = 0则会报错。
如果我们要自定义一些数据结构,使之能够跟以上的容器类型表现一样,那就需要去实现某些协议。
这里的协议跟其他语言中所谓的”接口”概念很像,一样的需要你去实现才行,只不过没那么正式而已。
如果要自定义不可变容器类型,只需要定义__len__ 和 __getitem__方法;
如果要自定义可变容器类型,还需要在不可变容器类型的基础上增加定义__setitem__ 和 __delitem__。
如果你希望你的自定义数据结构还支持”可迭代”, 那就还需要定义__iter__。
__len__(self)
需要返回数值类型,以表示容器的长度。该方法在可变容器和不可变容器中必须实现。
__getitem__(self, key)
当你执行self[key]的时候,调用的就是该方法。该方法在可变容器和不可变容器中也都必须实现。调用的时候,如果key的类型错误,该方法应该抛出typeerror;如果没法返回key对应的数值时,该方法应该抛出valueerror。
__setitem__(self, key, value)
当你执行self[key] = value时,调用的是该方法。
__delitem__(self, key)
当你执行del self[key]的时候,调用的是该方法。
__iter__(self)
该方法需要返回一个迭代器(iterator)。当你执行for x in container: 或者使用iter(container)时,该方法被调用。
__reversed__(self)
如果想要该数据结构被內建函数reversed()支持,就还需要实现该方法。
__contains__(self, item)
如果定义了该方法,那么在执行item in container 或者 item not in container时该方法就会被调用。如果没有定义,那么python会迭代容器中的元素来一个一个比较,从而决定返回true或者false。
__missing__(self, key)
dict字典类型会有该方法,它定义了key如果在容器中找不到时触发的行为。比如d = {‘a’: 1}, 当你执行d[notexist]时,d.__missing__[‘notexist’]就会被调用。
下面举例,使用上面讲的魔术方法来实现haskell语言中的一个数据结构。
# -*- coding: utf-8 -*-
class functionallist:
”’ 实现了内置类型list的功能,并丰富了一些其他方法: head, tail, init, last, drop, take”’
def __init__(self, values=none):
if values is 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 functionallist(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]
我们再举个例子,实现perl语言的autovivification,它会在你每次引用一个值未定义的属性时为你自动创建数组或者字典。
class autovivification(dict):
“””implementation of perl’s autovivification feature.”””
def __missing__(self, key):
value = self[key] = type(self)()
return value
weather = autovivification()
weather[‘china’][‘guangdong’][‘shenzhen’] = ‘sunny’
weather[‘china’][‘hubei’][‘wuhan’] = ‘windy’
weather[‘usa’][‘california’][‘los angeles’] = ‘sunny’
print weather
结果输出:{‘china’: {‘hubei’: {‘wuhan’: ‘windy’}, ‘guangdong’: {‘shenzhen’: ‘sunny’}}, ‘usa’: {‘california’: {‘los angeles’: ‘sunny’}}}
在python中,关于自定义容器的实现还有更多实用的例子,但只有很少一部分能够集成在python标准库中,比如counter, ordereddict等
上下文管理
with声明是从python2.5开始引进的关键词。你应该遇过这样子的代码:
with open(‘foo.txt’) as bar:
# do something with bar
在with声明的代码段中,我们可以做一些对象的开始操作和清除操作,还能对异常进行处理。
这需要实现两个魔术方法: __enter__ 和 __exit__。
__enter__(self)
__enter__会返回一个值,并赋值给as关键词之后的变量。在这里,你可以定义代码段开始的一些操作。
__exit__(self, exception_type, exception_value, traceback)
__exit__定义了代码段结束后的一些操作,可以这里执行一些清除操作,或者做一些代码段结束后需要立即执行的命令,比如文件的关闭,socket断开等。如果代码段成功结束,那么exception_type, exception_value, traceback 三个参数传进来时都将为none。如果代码段抛出异常,那么传进来的三个参数将分别为: 异常的类型,异常的值,异常的追踪栈。
如果__exit__返回true, 那么with声明下的代码段的一切异常将会被屏蔽。
如果__exit__返回none, 那么如果有异常,异常将正常抛出,这时候with的作用将不会显现出来。
举例说明:
这该示例中,indexerror始终会被隐藏,而typeerror始终会抛出。
class demomanager(object):
def __enter__(self):
pass
def __exit__(self, ex_type, ex_value, ex_tb):
if ex_type is indexerror:
print ex_value.__class__
return true
if ex_type is typeerror:
print ex_value.__class__
return # return none
with demomanager() as nothing:
data = [1, 2, 3]
data[4] # raise indexerror, 该异常被__exit__处理了
with demomanager() as nothing:
data = [1, 2, 3]
data[‘a’] # raise typeerror, 该异常没有被__exit__处理
输出:
traceback (most recent call last):
…对象的序列化
python对象的序列化操作是pickling进行的。pickling非常的重要,以至于python对此有单独的模块pickle,还有一些相关的魔术方法。使用pickling, 你可以将数据存储在文件中,之后又从文件中进行恢复。
下面举例来描述pickle的操作。从该例子中也可以看出,如果通过pickle.load 初始化一个对象, 并不会调用__init__方法。
# -*- coding: utf-8 -*-
from datetime import datetime
import pickle
class distance(object):
def __init__(self, meter):
print ‘distance __init__’
self.meter = meter
data = {
‘foo’: [1, 2, 3],
‘bar’: (‘hello’, ‘world!’),
‘baz’: true,
‘dt’: datetime(2016, 10, 01),
‘distance’: distance(1.78),
}
print ‘before dump:’, data
with open(‘data.pkl’, ‘wb’) as jar:
pickle.dump(data, jar) # 将数据存储在文件中
del data
print ‘data is deleted!’
with open(‘data.pkl’, ‘rb’) as jar:
data = pickle.load(jar) # 从文件中恢复数据
print ‘after load:’, data
值得一提,从其他文件进行pickle.load操作时,需要注意有恶意代码的可能性。另外,python的各个版本之间,pickle文件可能是互不兼容的。
pickling并不是python的內建类型,它支持所有实现pickle协议(可理解为接口)的类。pickle协议有以下几个可选方法来自定义python对象的行为。
__getinitargs__(self)
如果你希望unpickle时,__init__方法能够调用,那么就需要定义__getinitargs__, 该方法需要返回一系列参数的元组,这些参数就是传给__init__的参数。
该方法只对old-style class有效。所谓old-style class,指的是不继承自任何对象的类,往往定义时这样表示: class a:, 而非class a(object):
__getnewargs__(self)
跟__getinitargs__很类似,只不过返回的参数元组将传值给__new__
__getstate__(self)
在调用pickle.dump时,默认是对象的__dict__属性被存储,如果你要修改这种行为,可以在__getstate__方法中返回一个state。state将在调用pickle.load时传值给__setstate__
__setstate__(self, state)
一般来说,定义了__getstate__,就需要相应地定义__setstate__来对__getstate__返回的state进行处理。
__reduce__(self)
如果pickle的数据包含了自定义的扩展类(比如使用c语言实现的python扩展类)时,就需要通过实现__reduce__方法来控制行为了。由于使用过于生僻,这里就不展开继续讲解了。
令人容易混淆的是,我们知道, reduce()是python的一个內建函数, 需要指出__reduce__并非定义了reduce()的行为,二者没有关系。
__reduce_ex__(self)
__reduce_ex__ 是为了兼容性而存在的, 如果定义了__reduce_ex__, 它将代替__reduce__ 执行。
下面的代码示例很有意思,我们定义了一个类slate(中文是板岩的意思)。这个类能够记录历史上每次写入给它的值,但每次pickle.dump时当前值就会被清空,仅保留了历史。
# -*- coding: utf-8 -*-
import pickle
import time
class slate:
”’class to store a string and a changelog, and forget its value when pickled.”’
def __init__(self, value):
self.value = value
self.last_change = time.time()
self.history = []
def change(self, new_value):
# 修改value, 将上次的valeu记录在history
self.history.append((self.last_change, self.value))
self.value = new_value
self.last_change = time.time()
def print_changes(self):
print ‘changelog for slate object:’
for k, v in self.history:
print ‘%s %s’ % (k, v)
def __getstate__(self):
# 故意不返回self.value和self.last_change,
# 以便每次unpickle时清空当前的状态,仅仅保留history
return self.history
def __setstate__(self, state):
self.history = state
self.value, self.last_change = none, none
slate = slate(0)
time.sleep(0.5)
slate.change(100)
time.sleep(0.5)
slate.change(200)
slate.change(300)
slate.print_changes() # 与下面的输出历史对比
with open(‘slate.pkl’, ‘wb’) as jar:
pickle.dump(slate, jar)
del slate # delete it
with open(‘slate.pkl’, ‘rb’) as jar:
slate = pickle.load(jar)
print ‘current value:’, slate.value # none
print slate.print_changes() # 输出历史记录与上面一致运算符相关的魔术方法
运算符相关的魔术方法实在太多了,也很好理解,不打算多讲。在其他语言里,也有重载运算符的操作,所以我们对这些魔术方法已经很了解了。
比较运算符__cmp__(self, other)
如果该方法返回负数,说明self < other; 返回正数,说明self > other; 返回0说明self == other。
强烈不推荐来定义__cmp__, 取而代之, 最好分别定义__lt__等方法从而实现比较功能。__cmp__在python3中被废弃了。
__eq__(self, other)
定义了比较操作符==的行为.
__ne__(self, other)
定义了比较操作符!=的行为.
__lt__(self, other)
定义了比较操作符的行为.
__le__(self, other)
定义了比较操作符=的行为.
下面我们定义一种类型word, 它会使用单词的长度来进行大小的比较, 而不是采用str的比较方式。但是为了避免 word(‘bar’) == word(‘foo’) 这种违背直觉的情况出现,并没有定义__eq__, 因此word会使用它的父类(str)中的__eq__来进行比较。
下面的例子中也可以看出: 在编程语言中, 如果a >=b and a 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) word(‘fool’) # true
print ‘bar >= foo:’, word(‘bar’) >= word(‘foo’) # true
print ‘bar