python魔术方法

Posted AI蜗牛之家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python魔术方法相关的知识,希望对你有一定的参考价值。

文章目录

看到一篇很不错的文章,之前自己也想整理一下来着,转载该文

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

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

而有些魔术方法,巧妙使用它可以构造出非常优美的代码,比如将复杂的逻辑封装成简单的API。

本文编辑的思路借鉴自Rafe Kettler的这篇博客: A Guide to Python Magic Methods,并补充了一些代码示例。

介绍的顺序大概是:常见的先介绍,越少见的越靠后讲。

1.构造和初始化

  • __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时,及时关闭结束的连接。

2.属性访问控制

总有人要吐槽Python缺少对于类的封装,比如希望Python能够定义私有属性,然后提供公共可访问的gettersetter。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__调用

3.描述器对象

我们从一个例子来入手,介绍什么是描述符,并介绍__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的实例赋值前, 我们认为meterfoot应该是各自类的实例对象, 但是输出却是数值。这是因为__get__发挥了作用.

我们只是修改了meter,并且将其赋值成为int,但foot也修改了。这是__set__发挥了作用.

描述器对象(MeterFoot)不能独立存在, 它需要被另一个所有者类(Distance)所持有。
描述器对象可以访问到其拥有者实例的属性,比如例子中Foot的instance.meter

在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。
在Django的ORM中, models.Model中的IntegerField等, 就是通过描述器来实现功能的。

一个类要成为描述器,必须实现__get__, __set__, __delete__ 中的至少一个方法。下面简单介绍下:

  • __get__(self, instance, owner)
    参数instance是拥有者类的实例。参数owner是拥有者类本身。__get__在其拥有者对其读值的时候调用。

  • __set__(self, instance, value)
    __set__在其拥有者对其进行修改值的时候调用。

  • __delete__(self, instance)
    __delete__在其拥有者对其进行删除的时候调用。

4.构造自定义容器(Container)

4.1.自定义容器

在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

4.2.迭代器

这里重点说一下迭代器,在实际运用中用途较大,因此总结一下迭代器用到的魔术方法,并且主要以代码例子进行解释。

4.2.1.__iter____next__

其实这里需要引入一个概念,叫迭代器,常见的就是我们在使用for语句的时候,python内部其实是把for后面的对象上使用了内建函数iter,比如:

a = [1, 2, 3]
for i in a:
    do_something()

其实在python内部进行了类似如下的转换:

a = [1, 2, 3]
for i in iter(a):
    do_something()

那么iter返回的是什么呢,就是一个迭代对象,它主要映射到了类里面的__iter__函数,此函数返回的是一个实现了__next__的对象。注意理解这句话,比如:

class B(object):
    def __next__(self):
        raise StopIteration

class A(object):
    def __iter__(self):
        return B()

我们可以看见,A这个类实现了一个__iter__函数,返回的是B()的实例对象,其中B里面实现了__next__这个函数。

下面引入几个概念:
Iterable: 有迭代能力的对象,一个类,实现了__iter__,那么就认为它有迭代能力,通常此函数必须返回一个实现了__next__的对象,如果自己实现了,你可以返回self,当然这个返回值不是必须的;
Iterator: 迭代器(当然也是Iterable),同时实现了__iter____next__的对象,缺少任何一个都不算是Iterator,比如上面例子中,A()可以是一个Iterable,但是A()B()都不能算是和Iterator,因为A只实现了__iter__,而B只实现了__next__()

我们可以使用collections里面的类型来进行验证:

class B(object):
    def __next__(self):
        raise StopIteration

class A(object):
    def __iter__(self):
        return B()


from collections.abc import *

a = A()
b = B()
print(isinstance(a, Iterable))
print(isinstance(a, Iterator))

print(isinstance(b, Iterable))
print(isinstance(b, Iterator))

##结果是:
'''
True
False
False
False
'''

让我们稍微对B这个类做一点修改:

class B(object):
    def __next__(self):
        raise StopIteration

    def __iter__(self):
        return None

class A(object):
    def __iter__(self):
        return B()


from collections.abc import *

a = A()
b = B()
print(isinstance(a, Iterable))
print(isinstance(a, Iterator))

print(isinstance(b, Iterable))
print(isinstance(b, Iterator))

’‘’
结果是:
True
False
True
True
’‘’

4.2.2.真正的迭代器

上面只是做了几个演示,这里具体说明一下:
当调用iter函数的时候,生成了一个迭代对象,要求__iter__必须返回一个实现了__next__的对象,我们就可以通过next函数访问这个对象的下一个元素了,并且在你不想继续有迭代的情况下抛出一个StopIteration的异常(for语句会捕获这个异常,并且自动结束for),下面实现了一个自己的类似range函数的功能。

class MyRange(object):
    def __init__(self, end):
        self.start = 0
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.start < self.end:
            ret = self.start
            self.start += 1
            return ret
        else:
            raise StopIteration

from collections.abc import *

a = MyRange(5)
print(isinstance(a, Iterable))
print(isinstance(a, Iterator))

for i in a:
    print(i)
‘’‘
结果是:
True
True
0
1
2
3
4
’‘’

接下来我们使用next函数模拟一次:

class MyRange(object):
    def __init__(self, end):
        self.start = 0
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.start < self.end:
            ret = self.start
            self.start += 1
            return ret
        else:
            raise StopIteration

a = MyRange(5)
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a)) # 其实到这里已经完成了,我们在运行一次查看异常

可以看见一个很明显的好处是,每次产生的数据,是产生一个用一个,什么意思呢,比如我要遍历[0, 1, 2, 3.....]一直到10亿,如果使用列表的方式,那么是会全部载入内存的,但是如果使用迭代器,可以看见,当用到了(也就是在调用了next)才会产生对应的数字,这样就可以节约内存了,这是一种懒惰的加载方式。

4.2.3.总结

可以使用collection.abs里面的IteratorIterable配合isinstance函数来判断一个对象是否是可迭代的,是否是迭代器对象
iter实际是映射到了__iter__函数
只要实现了__iter__的对象就是可迭代对象(Iterable),正常情况下,应该返回一个实现了__next__的对象(虽然这个要求不强制),如果自己实现了__next__,当然也可以返回自己
同时实现了__iter____next__的是迭代器(Iterator),当然也是一个可迭代对象了,其中__next__应该在迭代完成后,抛出一个StopIteration异常
for语句会自动处理这个StopIteration异常以便结束for循环
生成器相关的文档已经在这里

5.上下文管理

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__处理

'''
输出:
<type 'exceptions.IndexError'>
<type 'exceptions.TypeError'>
Traceback (most recent call last):
  ...
'''

6.对象的序列化

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!'

魔术方法

魔术方法

面向对象之魔术方法

php中的魔术方法(Magic methods)和魔术常亮

PHP之十六个魔术方法详细介绍

PHP魔术方法