九游娱乐:介绍Python的魔术方法 - Magic Method
有些魔术方法,我们可能以后一辈子都不会再遇到了,这里也就只是简单介绍下;
而有些魔术方法,巧妙使用它可以构造出非常优美的代码,比如将复杂的逻辑封装成简单的API。
__init__我们很熟悉了,它在对象初始化的时候调用,我们一般将它理解为构造函数.
__new__是用来创建类并返回这个类的实例, 而__init__只是将传入的参数来初始化该实例.
__new__在创建一个实例的过程中必定会被调用,但__init__就不一定,比如通过pickle.load的方式反序列化一个实例时就不会调用__init__。
__new__方法总是需要返回该类的一个实例,而__init__不能返回除了None的任何值。比如下面例子:
如果要讲解__new__,往往需要牵扯到metaclass(元类)的介绍。
如果你有兴趣深入,可以参考我的另一篇博客:理解Python的metaclass
在对象的生命周期结束时,__del__会被调用,可以将__del__理解为析构函数.
如果调用了foo.__del__(),对象本身仍然存在. 但是调用了del foo, 就再也没有foo这个对象了.
请注意,如果解释器退出的时候对象还存在,就不能保证__del__被确切的执行了。所以__del__并不能替代良好的编程习惯。
总有人要吐槽Python缺少对于类的封装,比如希望Python能够定义私有属性,然后提供公共可访问的getter和 setter。Python其实可以通过魔术方法来实现封装。
该方法定义了你试图访问一个不存在的属性时的行为。因此,重载该方法可以实现捕获错误拼写然后进行重定向, 或者对一些废弃的属性进行警告。
__setattr__是实现封装的解决方案,它定义了你对属性进行赋值和修改操作时的行为。
不管对象的某个属性是否存在,它都允许你为该属性进行赋值,因此你可以为属性的值进行自定义操作。有一点需要注意,实现__setattr__时要避免无限递归的错误,下面的代码示例中会提到。
__delattr__与__setattr__很像,只是它定义的是你删除属性时的行为。实现__delattr__是同时要避免无限递归的错误。
__getattribute__定义了你的属性被访问时的行为,相比较,__getattr__只有该属性不存在时才会起作用。
需要提醒的九游娱乐-官网app是,最好不要尝试去实现__getattribute__,因为很少见到这种做法,而且很容易出bug。
__delattr__如果在其实现中出现del self.name这样的代码也会出现无限递归错误,这是一样的原因。
我们从一个例子来入手,介绍什么是描述符,并介绍__get__,__set__,__delete__的使用。(放在这里介绍是为了跟上一小节介绍的魔术方法作对比)
在上面例子中,在还没有对Distance的实例赋值前, 我们认为meter和foot应该是各自类的实例对象, 但是输出却是数值。这是因为__get__发挥了作用.
我们只是修改了meter,并且将其赋值成为int,但foot也修改了。这是__set__发挥了作用.
描述器对象(Meter、Foot)不能独立存在, 它需要被另一个所有者类(Distance)所持有。
描述器对象可以访问到其拥有者实例的属性,比如例子中Foot的instance.meter。
在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。
一个类要成为描述器,必须实现__get__,__set__,__delete__中的至少一个方法。下面简单介绍下:
参数instance是拥有者类的实例。参数owner是拥有者类本身。__get__在其拥有者对其读值的时候调用。
可变容器和不可变容器的区别在于,不可变容器一旦赋值后,不可对其中的某个元素进行修改。
如果我们要自定义一些数据结构,使之能够跟以上的容器类型表现一样,那就需要去实现某些协议。
这里的协议跟其他语言中所谓的接口概念很像,一样的需要你去实现才行,只不过没那么正式而已。
如果要自定义不可变容器类型,只需要定义__len__和__getitem__方法;
如果要自定义可变容器类型,还需要在不可变容器类型的基础上增加定义__setitem__和__delitem__。
如果你希望你的自定义数据结构还支持可迭代, 那就还需要定义__iter__。
需要返回数值类型,以表示容器的长度。该方法在可变容器和不可变容器中必须实现。
当你执行self[key]的时候,调用的就是该方法。该方法在可变容器和不可变容器中也都必须实现。
如果想要该数据结构被內建函数reversed()支持,就还需要实现该方法。
如果没有定义,那么Python会迭代容器中的元素来一个一个比较,从而决定返回True或者False。
dict字典类型会有该方法,它定义了key如果在容器中找不到时触发的行为。
下面举例,使用上面讲的魔术方法来实现Haskell语言中的一个数据结构。
我们再举个例子,实现Perl语言的AutoVivification,它会在你每次引用一个值未定义的属性时为你自动创建数组或者字典。
在Python中,关于自定义容器的实现还有更多实用的例子,但只有很少一部分能够集成在Python标准库中,比如Counter, OrderedDict等
with声明是从Python2.5开始引进的关键词。你应该遇过这样子的代码:
在with声明的代码段中,我们可以做一些对象的开始操作和清除操作,还能对异常进行处理。
__enter__会返回一个值,并赋值给as关键词之后的变量。在这里,你可以定义代码段开始的一些操作。
__exit__定义了代码段结束后的一些操作,可以这里执行一些清除操作,或者做一些代码段结束后需要立即执行的命令,比如文件的关闭,socket断开等。如果代码段成功结束,那么exception_type, exception_value, traceback 三个参数传进来时都将为None。如果代码段抛出异常,那么传进来的三个参数将分别为: 异常的类型,异常的值,异常的追踪栈。
如果__exit__返回True, 那么with声明下的代码段的一切异常将会被屏蔽。
如果__exit__返回None, 那么如果有异常,异常将正常抛出,这时候with的作用将不会显现出来。
这该示例中,IndexError始终会被隐藏,而TypeError始终会抛出。
Python对象的序列化操作是pickling进行的。pickling非常的重要,以至于Python对此有单独的模块pickle,还有一些相关的魔术方法。使用pickling, 你可以将数据存储在文件中,之后又从文件中进行恢复。
下面举例来描述pickle的操作。从该例子中也可以看出,如果通过pickle.load 初始化一个对象, 并不会调用__init__方法。
值得一提,从其他文件进行pickle.load操作时,需要注意有恶意代码的可能性。另外,Python的各个版本之间,pickle文件可能是互不兼容的。
pickling并不是Python的內建类型,它支持所有实现pickle协议(可理解为接口)的类。pickle协议有以下几个可选方法来自定义Python对象的行为。
如果你希望unpickle时,__init__方法能够调用,那么就需要定义__getinitargs__, 该方法需要返回一系列参数的元组,这些参数就是传给__init__的参数。
如果pickle的数据包含了自定义的扩展类(比如使用C语言实现的Python扩展类)时,就需要通过实现__reduce__方法来控制行为了。由于使用过于生僻,这里就不展开继续讲解了。
令人容易混淆的是,我们知道,reduce()是Python的一个內建函数, 需要指出__reduce__并非定义了reduce()的行为,二者没有关系。
下面的代码示例很有意思,我们定义了一个类Slate(中文是板岩的意思)。这个类能够记录历史上每次写入给它的值,但每次pickle.dump时当前值就会被清空,仅保留了历史。
运算符相关的魔术方法实在太多了,也很好理解,不打算多讲。在其他语言里,也有重载运算符的操作,所以我们对这些魔术方法已经很了解了。
强烈不推荐来定义__cmp__, 取而代之, 最好分别定义__lt__等方法从而实现比较功能。
下面我们定义一种类型Word, 它会使用单词的长度来进行大小的比较, 而不是采用str的比较方式。
上面的代码非常正常地实现了some_object的__add__方法。那么如果遇到相反的情况呢?
实现了类型转化为complex(复数, 也即1+2j这样的虚数)的行为.
在切片运算中将对象转化为int, 因此该方法的返回值必须是int。用一个例子来解释这个用法。
显然不能。如果真的是这样的线]这样的写法也应该是通过的。而实际上,该写法会抛出TypeError:
__index__了。虽然list和dict都实现了__getitem__方法, 但是它们的实现方式是不一样的。如果希望上面例子能够正常执行, 需要实现Thing的
coerce()内建函数:官方文档上的解释是, coerce(x, y)返回一组数字类型的参数, 它们被转化为同一种类型,以便它们可以使用相同的算术运算符进行操作。如果过程中转化失败,抛出TypeError。
repr()时调用。str()和repr()都是返回一个代表该实例的字符串,主要区别在于: str()的返回值要方便人来看,而repr()的返回值要方便计算机看。
bool()时调用, 返回True或者False。你可能会问, 为什么不是命名为