深入了解python中的魔术方法

前言

在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 foodel fooprint 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: passdel 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.2808class distance(object): meter = meter() foot = foot()d = distance()print d.meter, d.foot # 0.0, 0.0d.meter = 1print d.meter, d.foot # 1.0 3.2808d.meter = 2print 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 valueweather = 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 nonewith 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 datetimeimport pickleclass distance(object): def __init__(self, meter): print ‘distance __init__’ self.meter = meterdata = { ‘foo’: [1, 2, 3], ‘bar’: (‘hello’, ‘world!’), ‘baz’: true, ‘dt’: datetime(2016, 10, 01), ‘distance’: distance(1.78),}print ‘before dump:’, datawith open(‘data.pkl’, ‘wb’) as jar: pickle.dump(data, jar) # 将数据存储在文件中del dataprint ‘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 pickleimport timeclass 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, noneslate = 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 itwith open(‘slate.pkl’, ‘rb’) as jar: slate = pickle.load(jar)print ‘current value:’, slate.value # noneprint 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’) # trueprint ‘bar >= foo:’, word(‘bar’) >= word(‘foo’) # trueprint ‘bar

Posted in 未分类

发表评论