Python魔术

1,948次阅读
没有评论

Contents

前言

在Python中,所有以”__”双下划线包起来的方法,都统称为”魔术方法”。比如我们接触最多的__init__.

有些魔术方法,我们可能以后一辈子都不会再遇到了,这里也就只是简单介绍下;

Python魔术

而有些魔术方法,巧妙使用它可以构造出非常优美的代码,比如将复杂的逻辑封装成简单的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的任何值。比如下面例子:

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span><span class="hljs-params">(object)</span>:</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'foo __init__'</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">None</span>  <span class="hljs-comment"># 必须返回None,否则抛TypeError</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__del__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'foo __del__'</span>

实际中,你很少会用到__new__,除非你希望能够控制类的创建。 如果要讲解__new__,往往需要牵扯到metaclass(元类)的介绍。 如果你有兴趣深入,可以参考我的另一篇博客: 理解Python的metaclass

对于__new__的重载,Python文档中也有了详细的介绍。

在对象的生命周期结束时, __del__会被调用,可以将__del__理解为”构析函数”. __del__定义的是当一个对象进行垃圾回收时候的行为。

有一点容易被人误解, 实际上,x.__del__() 并不是对于del x的实现,但是往往执行del x时会调用x.__del__().

怎么来理解这句话呢? 继续用上面的Foo类的代码为例:

foo = Foo()
foo.__del__()
<span class="hljs-keyword">print</span> foo
<span class="hljs-keyword">del</span> foo
<span class="hljs-keyword">print</span> foo  <span class="hljs-comment"># NameError, foo is not defined</span>

如果调用了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__的无限递归错误:

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__setattr__</span><span class="hljs-params">(self, name, value)</span>:</span>
    self.name = value
    <span class="hljs-comment"># 每一次属性赋值时, __setattr__都会被调用,因此不断调用自身导致无限递归了。</span>

因此正确的写法应该是:

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__setattr__</span><span class="hljs-params">(self, name, value)</span>:</span>
    self.__dict__[name] = value

__delattr__如果在其实现中出现del self.name 这样的代码也会出现”无限递归”错误,这是一样的原因。

下面的例子很好的说明了上面介绍的4个魔术方法的调用情况:

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Access</span><span class="hljs-params">(object)</span>:</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__getattr__</span><span class="hljs-params">(self, name)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'__getattr__'</span>
        <span class="hljs-keyword">return</span> super(Access, self).__getattr__(name)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__setattr__</span><span class="hljs-params">(self, name, value)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'__setattr__'</span>
        <span class="hljs-keyword">return</span> super(Access, self).__setattr__(name, value)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__delattr__</span><span class="hljs-params">(self, name)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'__delattr__'</span>
        <span class="hljs-keyword">return</span> super(Access, self).__delattr__(name)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__getattribute__</span><span class="hljs-params">(self, name)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'__getattribute__'</span>
        <span class="hljs-keyword">return</span> super(Access, self).__getattribute__(name)

access = Access()
access.attr1 = <span class="hljs-keyword">True</span>  <span class="hljs-comment"># __setattr__调用</span>
access.attr1  <span class="hljs-comment"># 属性存在,只有__getattribute__调用</span>
<span class="hljs-keyword">try</span>:
    access.attr2  <span class="hljs-comment"># 属性不存在, 先调用__getattribute__, 后调用__getattr__</span>
<span class="hljs-keyword">except</span> AttributeError:
    <span class="hljs-keyword">pass</span>
<span class="hljs-keyword">del</span> access.attr1  <span class="hljs-comment"># __delattr__调用</span>

描述器对象

我们从一个例子来入手,介绍什么是描述符,并介绍__get__, __set__, __delete__ 的使用。(放在这里介绍是为了跟上一小节介绍的魔术方法作对比)

我们知道,距离既可以用单位”米”表示,也可以用单位”英尺”表示。现在我们定义一个类来表示距离,它有两个属性: 米和英尺。

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meter</span><span class="hljs-params">(object)</span>:</span>
    <span class="hljs-string">'''Descriptor for a meter.'''</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, value=<span class="hljs-number">0.0</span>)</span>:</span>
        self.value = float(value)
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__get__</span><span class="hljs-params">(self, instance, owner)</span>:</span>
        <span class="hljs-keyword">return</span> self.value
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__set__</span><span class="hljs-params">(self, instance, value)</span>:</span>
        self.value = float(value)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foot</span><span class="hljs-params">(object)</span>:</span>
    <span class="hljs-string">'''Descriptor for a foot.'''</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__get__</span><span class="hljs-params">(self, instance, owner)</span>:</span>
        <span class="hljs-keyword">return</span> instance.meter * <span class="hljs-number">3.2808</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__set__</span><span class="hljs-params">(self, instance, value)</span>:</span>
        instance.meter = float(value) / <span class="hljs-number">3.2808</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Distance</span><span class="hljs-params">(object)</span>:</span>
    meter = Meter()
    foot = Foot()

d = Distance()
<span class="hljs-keyword">print</span> d.meter, d.foot  <span class="hljs-comment"># 0.0, 0.0</span>
d.meter = <span class="hljs-number">1</span>
<span class="hljs-keyword">print</span> d.meter, d.foot  <span class="hljs-comment"># 1.0 3.2808</span>
d.meter = <span class="hljs-number">2</span>
<span class="hljs-keyword">print</span> d.meter, d.foot  <span class="hljs-comment"># 2.0 6.5616</span>

在上面例子中,在还没有对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语言中的一个数据结构。

<span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FunctionalList</span>:</span>
    <span class="hljs-string">''' 实现了内置类型list的功能,并丰富了一些其他方法: head, tail, init, last, drop, take'''</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, values=None)</span>:</span>
        <span class="hljs-keyword">if</span> values <span class="hljs-keyword">is</span> <span class="hljs-keyword">None</span>:
            self.values = []
        <span class="hljs-keyword">else</span>:
            self.values = values

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__len__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> len(self.values)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__getitem__</span><span class="hljs-params">(self, key)</span>:</span>
        <span class="hljs-keyword">return</span> self.values[key]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__setitem__</span><span class="hljs-params">(self, key, value)</span>:</span>
        self.values[key] = value

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__delitem__</span><span class="hljs-params">(self, key)</span>:</span>
        <span class="hljs-keyword">del</span> self.values[key]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__iter__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> iter(self.values)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__reversed__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> FunctionalList(reversed(self.values))

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">append</span><span class="hljs-params">(self, value)</span>:</span>
        self.values.append(value)
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">head</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-comment"># 获取第一个元素</span>
        <span class="hljs-keyword">return</span> self.values[<span class="hljs-number">0</span>]
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">tail</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-comment"># 获取第一个元素之后的所有元素</span>
        <span class="hljs-keyword">return</span> self.values[<span class="hljs-number">1</span>:]
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-comment"># 获取最后一个元素之前的所有元素</span>
        <span class="hljs-keyword">return</span> self.values[:-<span class="hljs-number">1</span>]
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">last</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-comment"># 获取最后一个元素</span>
        <span class="hljs-keyword">return</span> self.values[-<span class="hljs-number">1</span>]
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">drop</span><span class="hljs-params">(self, n)</span>:</span>
        <span class="hljs-comment"># 获取所有元素,除了前N个</span>
        <span class="hljs-keyword">return</span> self.values[n:]
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">take</span><span class="hljs-params">(self, n)</span>:</span>
        <span class="hljs-comment"># 获取前N个元素</span>
        <span class="hljs-keyword">return</span> self.values[:n]

我们再举个例子,实现Perl语言的autovivification,它会在你每次引用一个值未定义的属性时为你自动创建数组或者字典。

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AutoVivification</span><span class="hljs-params">(dict)</span>:</span>
    <span class="hljs-string">"""Implementation of perl's autovivification feature."""</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__missing__</span><span class="hljs-params">(self, key)</span>:</span>
        value = self[key] = type(self)()
        <span class="hljs-keyword">return</span> value

weather = AutoVivification()
weather[<span class="hljs-string">'china'</span>][<span class="hljs-string">'guangdong'</span>][<span class="hljs-string">'shenzhen'</span>] = <span class="hljs-string">'sunny'</span>
weather[<span class="hljs-string">'china'</span>][<span class="hljs-string">'hubei'</span>][<span class="hljs-string">'wuhan'</span>] = <span class="hljs-string">'windy'</span>
weather[<span class="hljs-string">'USA'</span>][<span class="hljs-string">'California'</span>][<span class="hljs-string">'Los Angeles'</span>] = <span class="hljs-string">'sunny'</span>
<span class="hljs-keyword">print</span> weather

结果输出:{<span class="hljs-string">'china'</span>: {<span class="hljs-string">'hubei'</span>: {<span class="hljs-string">'wuhan'</span>: <span class="hljs-string">'windy'</span>}, <span class="hljs-string">'guangdong'</span>: {<span class="hljs-string">'shenzhen'</span>: <span class="hljs-string">'sunny'</span>}}, <span class="hljs-string">'USA'</span>:    {<span class="hljs-string">'California'</span>: {<span class="hljs-string">'Los Angeles'</span>: <span class="hljs-string">'sunny'</span>}}}

在Python中,关于自定义容器的实现还有更多实用的例子,但只有很少一部分能够集成在Python标准库中,比如Counter, OrderedDict等

上下文管理

with声明是从Python2.5开始引进的关键词。你应该遇过这样子的代码:

<span class="hljs-keyword">with</span> open(<span class="hljs-string">'foo.txt'</span>) as bar:
    # <span class="hljs-keyword">do</span> something <span class="hljs-keyword">with</span> 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始终会抛出。

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DemoManager</span><span class="hljs-params">(object)</span>:</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__enter__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">pass</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__exit__</span><span class="hljs-params">(self, ex_type, ex_value, ex_tb)</span>:</span>
        <span class="hljs-keyword">if</span> ex_type <span class="hljs-keyword">is</span> IndexError:
            <span class="hljs-keyword">print</span> ex_value.__class__
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">True</span>
        <span class="hljs-keyword">if</span> ex_type <span class="hljs-keyword">is</span> TypeError:
            <span class="hljs-keyword">print</span> ex_value.__class__
            <span class="hljs-keyword">return</span>  <span class="hljs-comment"># return None</span>

<span class="hljs-keyword">with</span> DemoManager() <span class="hljs-keyword">as</span> nothing:
    data = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]
    data[<span class="hljs-number">4</span>]  <span class="hljs-comment"># raise IndexError, 该异常被__exit__处理了</span>

<span class="hljs-keyword">with</span> DemoManager() <span class="hljs-keyword">as</span> nothing:
    data = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]
    data[<span class="hljs-string">'a'</span>]  <span class="hljs-comment"># raise TypeError, 该异常没有被__exit__处理</span>

输出:
<type <span class="hljs-string">'exceptions.IndexError'</span>>
<type <span class="hljs-string">'exceptions.TypeError'</span>>
Traceback (most recent call last):
  ...

对象的序列化

Python对象的序列化操作是pickling进行的。pickling非常的重要,以至于Python对此有单独的模块pickle,还有一些相关的魔术方法。使用pickling, 你可以将数据存储在文件中,之后又从文件中进行恢复。

下面举例来描述pickle的操作。从该例子中也可以看出,如果通过pickle.load 初始化一个对象, 并不会调用__init__方法。

<span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">import</span> pickle

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Distance</span><span class="hljs-params">(object)</span>:</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, meter)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'distance __init__'</span>
        self.meter = meter

data = {
    <span class="hljs-string">'foo'</span>: [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>],
    <span class="hljs-string">'bar'</span>: (<span class="hljs-string">'Hello'</span>, <span class="hljs-string">'world!'</span>),
    <span class="hljs-string">'baz'</span>: <span class="hljs-keyword">True</span>,
    <span class="hljs-string">'dt'</span>: datetime(<span class="hljs-number">2016</span>, <span class="hljs-number">10</span>, <span class="hljs-number">01</span>),
    <span class="hljs-string">'distance'</span>: Distance(<span class="hljs-number">1.78</span>),
}
<span class="hljs-keyword">print</span> <span class="hljs-string">'before dump:'</span>, data
<span class="hljs-keyword">with</span> open(<span class="hljs-string">'data.pkl'</span>, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> jar:
    pickle.dump(data, jar)  <span class="hljs-comment"># 将数据存储在文件中</span>

<span class="hljs-keyword">del</span> data
<span class="hljs-keyword">print</span> <span class="hljs-string">'data is deleted!'</span>

<span class="hljs-keyword">with</span> open(<span class="hljs-string">'data.pkl'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> jar:
    data = pickle.load(jar)  <span class="hljs-comment"># 从文件中恢复数据</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'after load:'</span>, 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时当前值就会被清空,仅保留了历史。

<span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-keyword">import</span> pickle
<span class="hljs-keyword">import</span> time

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Slate</span>:</span>
    <span class="hljs-string">'''Class to store a string and a changelog, and forget its value when pickled.'''</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span><span class="hljs-params">(self, value)</span>:</span>
        self.value = value
        self.last_change = time.time()
        self.history = []

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span><span class="hljs-params">(self, new_value)</span>:</span>
        <span class="hljs-comment"># 修改value, 将上次的valeu记录在history</span>
        self.history.append((self.last_change, self.value))
        self.value = new_value
        self.last_change = time.time()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_changes</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">print</span> <span class="hljs-string">'Changelog for Slate object:'</span>
        <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> self.history:
            <span class="hljs-keyword">print</span> <span class="hljs-string">'%s    %s'</span> % (k, v)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__getstate__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-comment"># 故意不返回self.value和self.last_change,</span>
        <span class="hljs-comment"># 以便每次unpickle时清空当前的状态,仅仅保留history</span>
        <span class="hljs-keyword">return</span> self.history

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__setstate__</span><span class="hljs-params">(self, state)</span>:</span>
        self.history = state
        self.value, self.last_change = <span class="hljs-keyword">None</span>, <span class="hljs-keyword">None</span>

slate = Slate(<span class="hljs-number">0</span>)
time.sleep(<span class="hljs-number">0.5</span>)
slate.change(<span class="hljs-number">100</span>)
time.sleep(<span class="hljs-number">0.5</span>)
slate.change(<span class="hljs-number">200</span>)
slate.change(<span class="hljs-number">300</span>)
slate.print_changes()  <span class="hljs-comment"># 与下面的输出历史对比</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">'slate.pkl'</span>, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> jar:
    pickle.dump(slate, jar)
<span class="hljs-keyword">del</span> slate  <span class="hljs-comment"># delete it</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">'slate.pkl'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> jar:
    slate = pickle.load(jar)
<span class="hljs-keyword">print</span> <span class="hljs-string">'current value:'</span>, slate.value  <span class="hljs-comment"># None</span>
<span class="hljs-keyword">print</span> slate.print_changes()  <span class="hljs-comment"># 输出历史记录与上面一致</span>

运算符相关的魔术方法

运算符相关的魔术方法实在太多了,也很好理解,不打算多讲。在其他语言里,也有重载运算符的操作,所以我们对这些魔术方法已经很了解了。

比较运算符

__cmp__(self, other)

如果该方法返回负数,说明self < other; 返回正数,说明self > other; 返回0说明self == other。 强烈不推荐来定义__cmp__, 取而代之, 最好分别定义__lt__等方法从而实现比较功能。 __cmp__在Python3中被废弃了。

__eq__(self, other)

定义了比较操作符==的行为.

__ne__(self, other)

定义了比较操作符!=的行为.

__lt__(self, other)

定义了比较操作符<的行为.

__gt__(self, other)

定义了比较操作符>的行为.

__le__(self, other)

定义了比较操作符<=的行为.

__ge__(self, other)

定义了比较操作符>=的行为.

下面我们定义一种类型Word, 它会使用单词的长度来进行大小的比较, 而不是采用str的比较方式。 但是为了避免 Word('bar') == Word('foo') 这种违背直觉的情况出现,并没有定义__eq__, 因此Word会使用它的父类(str)中的__eq__来进行比较。

下面的例子中也可以看出: 在编程语言中, 如果a >=b and a <= b, 并不能推导出a == b这样的结论。

<span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Word</span><span class="hljs-params">(str)</span>:</span>
    <span class="hljs-string">'''存储单词的类,定义比较单词的几种方法'''</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__new__</span><span class="hljs-params">(cls, word)</span>:</span>
        <span class="hljs-comment"># 注意我们必须要用到__new__方法,因为str是不可变类型</span>
        <span class="hljs-comment"># 所以我们必须在创建的时候将它初始化</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">' '</span> <span class="hljs-keyword">in</span> word:
            <span class="hljs-keyword">print</span> <span class="hljs-string">"Value contains spaces. Truncating to first space."</span>
            word = word[:word.index(<span class="hljs-string">' '</span>)]  <span class="hljs-comment"># 单词是第一个空格之前的所有字符</span>
        <span class="hljs-keyword">return</span> str.__new__(cls, word)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__gt__</span><span class="hljs-params">(self, other)</span>:</span>
        <span class="hljs-keyword">return</span> len(self) > len(other)
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__lt__</span><span class="hljs-params">(self, other)</span>:</span>
        <span class="hljs-keyword">return</span> len(self) < len(other)
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__ge__</span><span class="hljs-params">(self, other)</span>:</span>
        <span class="hljs-keyword">return</span> len(self) >= len(other)
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__le__</span><span class="hljs-params">(self, other)</span>:</span>
        <span class="hljs-keyword">return</span> len(self) <= len(other)

<span class="hljs-keyword">print</span> <span class="hljs-string">'foo < fool:'</span>, Word(<span class="hljs-string">'foo'</span>) < Word(<span class="hljs-string">'fool'</span>)  <span class="hljs-comment"># True</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'foolish > fool:'</span>, Word(<span class="hljs-string">'foolish'</span>) > Word(<span class="hljs-string">'fool'</span>)  <span class="hljs-comment"># True</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'bar >= foo:'</span>, Word(<span class="hljs-string">'bar'</span>) >= Word(<span class="hljs-string">'foo'</span>)  <span class="hljs-comment"># True</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'bar <= foo:'</span>, Word(<span class="hljs-string">'bar'</span>) <= Word(<span class="hljs-string">'foo'</span>)  <span class="hljs-comment"># True</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'bar == foo:'</span>, Word(<span class="hljs-string">'bar'</span>) == Word(<span class="hljs-string">'foo'</span>)  <span class="hljs-comment"># False, 用了str内置的比较方法来进行比较</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'bar != foo:'</span>, Word(<span class="hljs-string">'bar'</span>) != Word(<span class="hljs-string">'foo'</span>)  <span class="hljs-comment"># True</span>

一元运算符和函数

__pos__(self)

实现了’+’号一元运算符(比如+some_object)

__neg__(self)

实现了’-‘号一元运算符(比如-some_object)

__invert__(self)

实现了~号一元运算符(比如~some_object)

__abs__(self)

实现了abs()內建函数.

__round__(self, n)

实现了round()内建函数. 参数n表示四舍五进的精度.

__floor__(self)

实现了math.round(), 向下取整.

__ceil__(self)

实现了math.ceil(), 向上取整.

__trunc__(self)

实现了math.trunc(), 向0取整.

算术运算符

__add__(self, other)

实现了加号运算.

__sub__(self, other)

实现了减号运算.

__mul__(self, other)

实现了乘法运算.

__floordiv__(self, other)

实现了//运算符.

__div__(self, other)

实现了/运算符. 该方法在Python3中废弃. 原因是Python3中,division默认就是true division.

__truediv__(self, other)

实现了true division. 只有你声明了from __future__ import division该方法才会生效.

__mod__(self, other)

实现了%运算符, 取余运算.

__divmod__(self, other)

实现了divmod()內建函数.

__pow__(self, other)

实现了**操作. N次方操作.

__lshift__(self, other)

实现了位操作<<.

__rshift__(self, other)

实现了位操作>>.

__and__(self, other)

实现了位操作&.

__or__(self, other)

实现了位操作|

__xor__(self, other)

实现了位操作^

反算术运算符

这里只需要解释一下概念即可。假设针对some_object这个对象:

<span class="hljs-title">some_object</span> + other

上面的代码非常正常地实现了some_object的__add__方法。那么如果遇到相反的情况呢?

<span class="hljs-title">other</span> + some_object

这时候,如果other没有定义__add__方法,但是some_object定义了__radd__, 那么上面的代码照样可以运行。 这里的__radd__(self, other)就是__add__(self, other)的反算术运算符。

所以,类比的,我们就知道了更多的反算术运算符, 就不一一展开了:

  • __rsub__(self, other)
  • __rmul__(self, other)
  • __rmul__(self, other)
  • __rfloordiv__(self, other)
  • __rdiv__(self, other)
  • __rtruediv__(self, other)
  • __rmod__(self, other)
  • __rdivmod__(self, other)
  • __rpow__(self, other)
  • __rlshift__(self, other)
  • __rrshift__(self, other)
  • __rand__(self, other)
  • __ror__(self, other)
  • __rxor__(self, other)

增量赋值

这也是只要理解了概念就容易掌握的运算。举个例子:

<span class="hljs-title">x</span> = <span class="hljs-number">5</span>
x += <span class="hljs-number">1</span>  <span class="hljs-comment"># 这里的+=就是增量赋值,将x+1赋值给了x</span>

因此对于a += b, __iadd__ 将返回a + b, 并赋值给a。 所以很容易理解下面的魔术方法了:

  • __iadd__(self, other)
  • __isub__(self, other)
  • __imul__(self, other)
  • __ifloordiv__(self, other)
  • __idiv__(self, other)
  • __itruediv__(self, other)
  • __imod__(self, other)
  • __ipow__(self, other)
  • __ilshift__(self, other)
  • __irshift__(self, other)
  • __iand__(self, other)
  • __ior__(self, other)
  • __ixor__(self, other)

类型转化

__int__(self)

实现了类型转化为int的行为.

__long__(self)

实现了类型转化为long的行为.

__float__(self)

实现了类型转化为float的行为.

__complex__(self)

实现了类型转化为complex(复数, 也即1+2j这样的虚数)的行为.

__oct__(self)

实现了类型转化为八进制数的行为.

__hex__(self)

实现了类型转化为十六进制数的行为.

__index__(self)

在切片运算中将对象转化为int, 因此该方法的返回值必须是int。用一个例子来解释这个用法。

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Thing</span><span class="hljs-params">(object)</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__index__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>

thing = Thing()
list_ = [<span class="hljs-string">'a'</span>, <span class="hljs-string">'b'</span>, <span class="hljs-string">'c'</span>]
<span class="hljs-keyword">print</span> list_[thing]  <span class="hljs-comment"># 'b'</span>
<span class="hljs-keyword">print</span> list_[thing:thing]  <span class="hljs-comment"># []</span>

上面例子中, list_[thing]的表现跟list_[1]一致,正是因为Thing实现了__index__方法。

可能有的人会想,list_[thing]为什么不是相当于list_[int(thing)]呢? 通过实现Thing的__int__方法能否达到这个目的呢?

显然不能。如果真的是这样的话,那么list_[1.1:2.2]这样的写法也应该是通过的。 而实际上,该写法会抛出TypeError: slice indices must be integers or None or have an __index__ method

下面我们再做个例子,如果对一个dict对象执行dict_[thing]会怎么样呢?

dict_ = {<span class="hljs-number">1</span>: <span class="hljs-string">'apple'</span>, <span class="hljs-number">2</span>: <span class="hljs-string">'banana'</span>, <span class="hljs-number">3</span>: <span class="hljs-string">'cat'</span>}
<span class="hljs-built_in">print</span> dict_[thing]  <span class="hljs-comment"># raise KeyError</span>

这个时候就不是调用__index__了。虽然listdict都实现了__getitem__方法, 但是它们的实现方式是不一样的。 如果希望上面例子能够正常执行, 需要实现Thing的__hash____eq__方法.

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Thing</span><span class="hljs-params">(object)</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__hash__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__eq__</span><span class="hljs-params">(self, other)</span>:</span>
        <span class="hljs-keyword">return</span> hash(self) == hash(other)

dict_ = {<span class="hljs-number">1</span>: <span class="hljs-string">'apple'</span>, <span class="hljs-number">2</span>: <span class="hljs-string">'banana'</span>, <span class="hljs-number">3</span>: <span class="hljs-string">'cat'</span>}
<span class="hljs-keyword">print</span> dict_[thing]  <span class="hljs-comment"># apple</span>

__coerce__(self, other)

实现了混合模式运算。

要了解这个方法,需要先了解coerce()内建函数: 官方文档上的解释是, coerce(x, y)返回一组数字类型的参数, 它们被转化为同一种类型,以便它们可以使用相同的算术运算符进行操作。如果过程中转化失败,抛出TypeError。

比如对于coerce(10, 10.1), 因为10和10.1在进行算术运算时,会先将10转为10.0再来运算。因此coerce(10, 10.1)返回值是(10.0, 10.1).

__coerce__在Python3中废弃了。

其他魔术方法

还没讲到的魔术方法还有很多,但有些我觉得很简单,或者很少见,就不再累赘展开说明了。

__str__(self)

对实例使用str()时调用。

__repr__(self)

对实例使用repr()时调用。str()repr()都是返回一个代表该实例的字符串, 主要区别在于: str()的返回值要方便人来看,而repr()的返回值要方便计算机看。

__unicode__(self)

对实例使用unicode()时调用。unicode()str()的区别在于: 前者返回值是unicode, 后者返回值是str。unicode和str都是basestring的子类。

当你对一个类只定义了__str__但没定义__unicode__时,__unicode__会根据__str__的返回值自动实现,即return unicode(self.__str__()); 但返回来则不成立。

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StrDemo2</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__str__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">'StrDemo2'</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StrDemo3</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__unicode__</span><span class="hljs-params">(self)</span>:</span>
        <span class="hljs-keyword">return</span> <span class="hljs-string">u'StrDemo3'</span>

demo2 = StrDemo2()
<span class="hljs-keyword">print</span> str(demo2)  <span class="hljs-comment"># StrDemo2</span>
<span class="hljs-keyword">print</span> unicode(demo2)  <span class="hljs-comment"># StrDemo2</span>

demo3 = StrDemo3()
<span class="hljs-keyword">print</span> str(demo3)  <span class="hljs-comment"># <__main__.StrDemo3 instance></span>
<span class="hljs-keyword">print</span> unicode(demo3)  <span class="hljs-comment"># StrDemo3</span>

__format__(self, formatstr)

"Hello, {0:abc}".format(a)等价于format(a, "abc"), 等价于a.__format__("abc")

这在需要格式化展示对象的时候非常有用,比如格式化时间对象。

__hash__(self)

对实例使用hash()时调用, 返回值是数值类型。

__nonzero__(self)

对实例使用bool()时调用, 返回True或者False。 你可能会问, 为什么不是命名为__bool__? 我也不知道。 我只知道该方法在Python3中改名为__bool__了。

__dir__(self)

对实例使用dir()时调用。通常实现该方法是没必要的。

__sizeof__(self)

对实例使用sys.getsizeof()时调用。返回对象的大小,单位是bytes。

__instancecheck__(self, instance)

对实例调用isinstance(instance, class)时调用。 返回值是布尔值。它会判断instance是否是该类的实例。

__subclasscheck__(self, subclass)

对实例使用issubclass(subclass, class)时调用。返回值是布尔值。它会判断subclass否是该类的子类。

__copy__(self)

对实例使用copy.copy()时调用。返回”浅复制”的对象。

__deepcopy__(self, memodict={})

对实例使用copy.deepcopy()时调用。返回”深复制”的对象。

__call__(self, [args...])

该方法允许类的实例跟函数一样表现:

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">XClass</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__call__</span><span class="hljs-params">(self, a, b)</span>:</span>
        <span class="hljs-keyword">return</span> a + b

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add</span><span class="hljs-params">(a, b)</span>:</span>
    <span class="hljs-keyword">return</span> a + b

x = XClass()
<span class="hljs-keyword">print</span> <span class="hljs-string">'x(1, 2)'</span>, x(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)
<span class="hljs-keyword">print</span> <span class="hljs-string">'callable(x)'</span>, callable(x)  <span class="hljs-comment"># True</span>
<span class="hljs-keyword">print</span> <span class="hljs-string">'add(1, 2)'</span>, add(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)
<span class="hljs-keyword">print</span> <span class="hljs-string">'callable(add)'</span>, callable(add)  <span class="hljs-comment"># True</span>

Python3中的差异

  • Python3中,str与unicode的区别被废除了,因而__unicode__没有了,取而代之地出现了__bytes__.
  • Python3中,division默认就是true division, 因而__div__废弃.
  • __coerce__因存在冗余而废弃.
  • __cmp__因存在冗余而废弃.
  • __nonzero__改名为__bool__.
admin
版权声明:本站原创文章,由admin2016-11-07发表,共计16244字。
转载提示:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)