Python 魔法方法指南

Reproduce from

简介

什么是魔法方法呢?他们在面向对象的 Python 处处皆是。他们是一些可以让你对类添加「魔法」的特殊 function。他们经常是两个下划线包围来命名的(比如 __init____lt__)。

构造方法

最被熟知的基本魔法方法就是 init,我们可以用它来指明一个对象初始化的行为。然而,当我们调用 x = SomeClass() 的时候,init 并不是第一个被调用的方法。事实上,第一个被调用的是 new,这个方法才真正地创建了实例。当这个对象的生命周期结束的时候,del 会被调用。

  • __new__(cls, [...])
    __new__ 是对象实例话时第一个调用的方法,它只取 cls 参数,并把其他参数传给 __init____new__ 很少使用,但是也有它适合的场景,尤其是当类继承自一个像元组或者字符串这样不经常改变的类型的时候。
  • __init__(self, [...])
    类的初始化方法。它获取任何传给构造器的参数(比如调用 x = SomeClass(10, 'foo')),__init__ 就会接到参数 10foo__init__ 在 Python 的定义中用的最多。
  • __del__(self)
    __new____init__ 是对象的构造器。__del__ 是对象的销毁器。它并非实现了语句 del x(因此该语句不等同于 x.__del__())。而是定义了当对象被垃圾回收时的行为。当对象需要在销毁时做一些处理的时候,这个方法很有用,比如 socket 对象,文件对象。但是需要注意的是,当 Python 解释器对出但对象仍然存活的时候,__del__ 并不会执行。所以养成手工清理的好习惯是很重要的,比如即使关闭连接。
class FileObject(object):
    '''文件对象的装饰类,用来保证文件被删除时能够正确关闭。'''
    def __init__(self, file_path='~', file_name='sample.text'):
        # 使用读写模式打开filepath中的filename文件
        self.file = open(join(file_path, file_name), 'r+')

    def __del__(self):
        self.file.close()
        def self.file

操作符

使用Python魔法方法的一个巨大优势就是可以构建一个拥有Python内置类型行为的对象。这意味着你可以避免使用非标准的、丑陋的方式来表达简单的操作。 在一些语言中,这样做很常见:

if instance.equals(other_instance):
  # do something

你当然可以在 Python 里也这么做,但是这张做让代码变的冗长而混乱。不同的类库可能对同一种比较操作采用不同的方法名称,这让使用者需要很多没有必要的工作。运用魔法方法的魔力,可以定义方法 __eq__

if instance == other_instance:
  # do something

比较操作符

Python 包含了一系列的魔法方法,用于实现对象之间直接比较,而不需要采用方法调用。同样也可以重载 Python 默认的比较方法,改变他们的行为。

  • __cmp__(self, other)
    __cmp__ 是所有比较魔法方法中最基础的一个,它实际上定义了所有比较操作符的行为(<,==,!=,等等),但是他可能不能按照你需要的方式工作(例如,判断一个实例和另一个实例是否相等采用一套标准,而与判断一个实例是否大于另一实例采用另一套)。__cmp__ 应该在 self < other 是返回一个负整数,在 self == other 时返回0,在 self > other 时返回正整数。最好只定义你所需要的比较形式,而不一次定义全部。如果你需要实现所有的比较形式,而且他们的判断标准类似,那么 __cmp__ 是一个很好的方法,可以减少代码重复,让代码更简洁。
  • __eq__(self, other)
    定义等于操作符 == 的行为。
  • __ne__(self, other)
    定于不等于操作符 != 的行为。
  • __lt__(self, other)
    定义小于操作符 < 的行为。
  • __gt__(self, other)
    定义大于操作符 > 的行为。
  • __le__(self, other)
    定义小于等于操作符 <= 的行为。
  • __ge__(self, other)
    定义大于等于操作符 >= 的行为。

假如我们用一个类来存储单词。我们可能想按照字典序(字母顺序)来比较单词,字符串的默认比较行为就是这样。我们可能也想按照其他规则来比较字符串,像是长度,或者音节的数量。在这个例子中,我们使用长度作为比较标准,下面是一种实现:

class Word(str):
    '''单词类,按照单词长度来定义比较行为'''
    def __new__(cls, word):
        # 注意,我们只能使用 __new__,因为 str 是不可变类型
        # 所以我们必须提前初始化它(在实例创建时)
        if ' ' in word:
            print("Value contains spaces. Truncating to first space.")
            word = word[:word.index(' ')]
            # Word现在包含第一个空格前的所有字母
        return str.__new__(cls, word)
    def __gt__(self, other):
        return len(self) > 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) <= len(other)

现在我们可以创建两个 Word 对象(Word('foo') 和 Word('bar'))然后根据长度来比较他们。注意我们没有定义 __eq____ne__,这是因为有时候他们会导致奇怪的结果(Word('foo') == Word('bar') 得到的结果会是 True)。根据长度测试是否相等毫无意义,所以我们使用 str 的实现来比较相等。

不需要实现所有的比较方法,就可以使用丰富的比较操作。标准库还在 functools 模块中提供了一个类装饰器,只要我们定义__eq__ 和 另一个操作符(__gt____lt__ 等),它就可以帮我们实现比较方法。这个特性只在 Python 2.7 中可用。当它可用时,它能帮助我们节省大量的时间和精力。要使用它,只需要它 @total_ordering 放在类的定义之上就可以了。

数值操作符

就像你可以使用比较操作符来比较类的实例,你也可以定义数值操作符的行为。这样的操作符真的很多。把它们分为五类:一元操作符,常见算数操作符,反射算数操作符(后面会涉及更多),增强赋值操作符,和类型转换操作符。

一元操作符

一元操作符只有一个操作符。

  • __pos__(self)
    实现取正操作,例如 +some_object
  • __neg__(self)
    实现取负操作,例如 -some_object
  • __abs__(self)
    实现内建绝对值函数 abs() 操作
  • __invert__(self)
    实现取反操作符 ~
  • __rount__(self, n)
    实现内建函数 round(),n 是近似小数点的位数
  • __floor__(self)
    实现 math.floor() 函数,即向下取整
  • __ceil__(self)
    实现 math.ceil() 函数,即向上取整
  • __trunc__(self)
    实现 math.trunc() 函数,即距离零最近的整数

常见算数操作符

常见的二元操作符(和一些函数),像 +,-,* 之类的,它们很容易从字面意思理解。

  • __add__(self, other)
    实现加法操作
  • __sub__(self, other)
    实现减法操作
  • __mul__(self, other)
    实现乘法操作
  • __floordiv__(self, other)
    实现使用 // 操作符的整数除法
  • __div__(self, other)
    实现使用 / 操作符的除法
  • __truediv__(self, other)
    实现 true 除法,这个函数只有使用 from __future__ import division 时才有作用
  • __mod__(self, other)
    实现 % 取余操作符
  • __divmod__(self, other)
    实现 divmod 内建函数
  • __pow__
    实现 ** 操作符
  • __lshift__(self, other)
    实现左移位运算符 <<
  • __rshift__(self, other)
    实现右移位运算符 >>
  • __and__(self, other)
    实现按位与运算符 &
  • __or__(self, other)
    实现按位或运算符 |
  • __xor__(self, other)
    实现按位异或运算符 ^

反射算数运算符

反射运算符,举个例子:

some_ohter + other

这是"常见"的加法,反射是一样的意思,只不过运算符交换了一下位置:

other + some_object

所有反射运算符魔法方法和它们的常见版本做的工作相同,只不过是处理交换两个操作数之后的情况。绝大多数情况下,反射运算和正常顺序产生的结果是相同的,所以很可能定义 radd 时只是调用一下 add。注意,操作符左侧的对象(也就是上面的 other)一定不要定义(或产生 NotImplemented 异常)操作符的非反射版本。例如,在上面的例子中,只有当 other 没有定义 addsome_other.radd 才会被调用。

  • __radd__(self, other)
    实现反射加法操作
  • __rsub__(self, other)
    实现反射减法操作
  • __rmul__(self, other)
    实现反射乘法操作
  • __rfloordiv__(self, other)
    实现使用 // 操作符的整数反射除法
  • __rdiv__(self, other)
    实现使用 / 操作符的反射除法
  • __truediv__(self, other)
    实现 true 反射除法,这个函数只有使用 from __future__ import division 时才有作用
  • __rmod__(self, other)
    实现 % 反射取余操作符
  • __rdivmod__(self, other)
    实现调用 divmod(other, self)divmod 内建函数的操作。
  • __rpow__
    实现 ** 反射操作符
  • __rlshift__(self, other)
    实现反射左移位运算符 <<
  • __rrshift__(self, other)
    实现反射右移位运算符 >>
  • __rand__(self, other)
    实现反射按位与运算符 &
  • __ror__(self, other)
    实现反射按位或运算符 |
  • __rxor__(self, other)
    实现反射按位异或运算符 ^

增强赋值运算符

Python 提供了大量的魔术方法,可以用来自定义增强赋值操作的行为。它融合了 "常见" 的操作符和复制操作,例子:

x = 5
x += 1  # 也就是 x = x + 1

这些方法都应该返回左侧操作数应该被赋予的值(例如,a += b __iadd__ 也许会返回 a + b,这个结果会赋给 a),下面是方法列表:

  • __iadd__(self, other)
    实现加法复制操作
  • __isub__(self, other)
    实现减法赋值操作
  • __imul__(self, other)
    实现乘法赋值操作
  • __ifloordiv__(self, other)
    实现使用 //= 操作符的整数除法赋值操作
  • __idiv__(self, other)
    实现使用 /= 操作符的除法赋值操作
  • __iturediv__(self, other)
    实现 true 除法赋值操作,这个函数只有使用 from __future__ import division 时才有作用
  • __imod__(self, other)
    实现实现 %= 取余赋值操作
  • __ipow__
    实现 **= 操作
  • __ilshift__(self, other)
    实现左移位赋值运算符 <<=
  • __irshift__(self, other)
    实现右移位赋值运算符 >>=
  • __iand__(self, other)
    实现按位与运算符 &=
  • __ior__(self, other)
    实现按位异或赋值运算符 |
  • __ixor__(self, other)
    实现按位异或赋值运算符 ^=

类型转换运算符

Python 也有一系列的魔法方法用于实现类似 float() 的内建类型转换函数的操作:

  • __int__(self)
    实现到 int 类型的转换
  • __long__(self)
    实现到 long 的类型转换
  • __float__(self)
    实现到 float 的类型转换
  • __complex__(self)
    实现到 complex 的类型转换
    ...
  • __index__(self)
    实现当对象用于切片表示时到一个整数的类型转换。如果你定义了一个可能会用于切片操作的数值类型,你应该定义 __index__
  • __trunc__(self)
    当调用 math.trunc(self) 时调用该方法,__trunc__ 应该返回 self 截取到一个整数类型(通常是 long 类型)的值
  • __coerce__(self)
    该方法用于实现混合模式算术运算,如果不能进行类型转换,__coerce__ 应该返回 None。反之,它应该返回一个二元组 selfother,这两者均已被转换成相同的类型

类的表示

使用字符串来表示类是一个相当有用的特性。在 Python 中有一些内建方法可以返回类的表示,相对应的,也有一系列魔法方法可以用来自定义在使用这些内建函数式类的行为。

  • __str__(self)
    定义对类的实例调用 str() 时的行为
  • __repr__(self)
    定义对类的实例调用 repr() 时的行为。str()repr() 最主要的差别在于 “目标用户”。repr() 的作用是产生机器可读的输出(在大部分情况下,起输出可以作为有效的 Python 代码),而 str() 侧产生人类可读的输出。
  • __unicode__(self)
    定义对类的实例调用 unicode() 时的行为。unicode()str() 很像,只是它返回 unicode 字符串。注意,如果调用者试图调用 str() 而你的类只实现了 __unicode__ 字符串,那么类将不能正常工作。所以你应该总定义 __str__(),以防有些人没有使用 unicode()
  • __format__(self)
    定义当类的实例用于新式字符串格式化时的行为,例如,"Hello,{}:abc!".format("abc") 会导致调用 a.__format__("abc")。当定义你自己的数值类型或字符串类型时,你可能想提供某些特殊的格式化选项,这种情况下这个魔法方法会非常有用。
  • __hash__(self)
    定义对类的实例调用 hash() 时的行为。它必须返回一个整数,其结果会被用于字典中键的快速比较。同时注意一点,实现这个魔法方法通常也需要实现 __eq__,并且遵守如下的规则:a == b 意味着 hash(a) == hash(b)
  • __nonzero__(self)
    定义对类的实例调用 bool() 时的行为,根据你自己对类的设计,针对不同的实例,这个魔法方法应该相应地返回 TrueFalse
  • __dir__(self)
    定义对类的实例调用 dir() 时的行为,这个方法应该像调用者返回一个属性列表。一般来说,没必要自己实现 __dir__。但是如果你重定义了 __getattr__ 或者 __getattribute__,乃至使用动态生成的属性,以实现类的交互式使用,那么这个魔法方法是必不可少的。

访问控制

很多从其他语言转向 Python 的人抱怨 Python 的类缺少真正意义上的封装(即没办法定义私有属性然后使用共有的 getter 和 setter)。然而事实并非如此。实际上 Python 不是通过现实定义的字段和方法修改器,而是通过魔法方法实现了一系列的封装。

  • __getattr__(self, name)
    当用户试图访问一个根本不存在的(或者暂时不存在)属性时,你可以通过这个魔法方法来定义类的行为。这个可以用于捕捉错误的拼写并且给出指引,使用废弃属性时给出警告(如果你愿意,仍然可以计算并且返回该属性),以及灵活地处理 AttributeError。只有当试图访问不存在的属性时它才会被调用,所以这不能算是一个真正的封装的办法。
  • __setattr__(self, name, value)
    __getattr__ 不同,__setattr__ 可以用于真正意义上的封装。它允许你定义某个属性的赋值行为,不管这个属性存在与否,也就是说你可以对任意属性的任何变化都定义自己的规则。然后,一定要小心使用 __setattr__
  • __delattr__(self, name)
    这个魔法方法和 __setattr__ 几乎相同,只不过它是用于处理删除属性时的行为。和 __setattr__ 一样,使用它时也需要多加小心,防止产生无限递归(在 __delattr__ 的实现中调用 del self.name 会导致无限递归)。
  • __getattribute__(self, name)
    __getattribute__ 看起来和上面那些方法很合得来,但是最好不要使用它。__getattribute__ 只能用新式类。在最新版的 Python 中所有的类都是新式类。在最新版的 Python 中所有的类都是新式类,在老版 Python 中可以使用继承 object 来创建新式类。__getattribute__ 允许你自定义属性被访问时的行为,它也同样可能遇到无限递归问题(通过调用基类的 __getattribute__ 来避免)。__getattribute__基本上可以替代 __getattr__。只有当它被实现,并且显示地被调用,或者产生 AttributeError 时它才被使用。这个魔法方法可以被使用,不过不推荐使用它,因为它的使用范围相对有限(通常我们想要在赋值时进行特殊操作,而不是取值时),而且实现这个方法很容易出现 Bug。

自定义这些控制属性访问的魔法方法很容易导致问题,考虑下面这个例子:

def __setattr__(self, name, value):
    self.name = value
    # 因为每次属性赋值都要调用 __setattr__(),所以这里的实现会导致递归
    # 这里的调用实际上是 self.__setattr('name', value)。因为这个方法一直
    # 在调用自己,因此递归将持续进行,知道程序崩溃

def __setattr__(self, name, value):
    self.__dict__[name] = value  # 使用 __dict__ 进行赋值
    # 自定义行为

再次重申,Python 的魔法方法十分强大,能力越强责任越大,了解如何正确的使用魔法方法更加重要。

到这里,我们对 Python 的自定义属性存取控制有了什么样的印象?它并不适合轻度的使用。实际上,它有些过份强大,而且违反直觉。然而它之所以存在,是因为一个更大的原则:** Python 不指望杜绝坏事发生,而是想办法让做坏事变得困难。**自由是至高无上的权利,你真的可以随心所欲。下面的例子展示了实际应用中某些特殊的属性访问方法(注意我们之所以使用 super 是因为不是所有的类都 __dict__ 属性):

class AccessCounter(object):
    ''' 一个包含了一个只并且实现了访问计数器的类
    每次值的变化都会导致计数器自增'''
    def __init__(self, val):
        super(AccessCounter, self).__setattr__('counter', 0)
        super(AccessCounter, self).__setattr__('value', val)

    def __setattr__(self, name, value):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        # 使计数器自增变成不可避免
        # 如果你想阻止其他属性的赋值行为
        # 产生 AttributeError(name) 就可以了
        super(AccessCounter, self).__setattr__(name, value)

    def __delattr__(self, name):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
            super(AccessCounter, self).__delattr(name)

自定义序列 Making Custom Sequences

有许多魔法办法可以让你的 Python 类表现得像是内建序列类型(字典,元组,列表,字符串等)。他们给了你难以置信的控制能力,可以让你的类与一系列的全局函数完美结合。

预备知识 Requirements

既然讲到创建自己的序列类型,就不得不说协议 protocols。协议类似某些语言中的接口,里面包含的是一些必须实现的方法。在 Python 中,协议完全是非正式的(totally informal),也不需要显式的声明,实际上,他们更像是一种参考标准 guildlines。

在 Python 的实现自定义容器类型需要用到一些协议。首先,不可变容器类型有如下协议:想实现一个不可变容器,你需要定义 __len____getitem__。可变容器的协议除了上面的两个方法外,还需要定义 __setitem____delitem__。最后,如果想让对象可以迭代,需要定义 __iter__,这个方法返回一个迭代器。迭代器必须遵守迭代器协议,需要定义 __iter__(返回它自己)和 next 方法。

容器背后的魔法方法

  • __len__(self)
    返回容器的长度,可变和不可变类型都需要实现。
  • __getitem__(self, key)
    定义对容器中某一项使用 self[key] 的方式进行读取操作时的行为。这也是可变和不可变容器类型都需要实现的一个方法。它应该在键的类型错误时产生 TypeError 异常,同时在没有键值相匹配的内容是产生 KeyError 异常。
  • __setitem__(self, key)
    定义对容器中某一项使用 self[key] 的方式进行赋值操作时的行为。它是可变容器类型必须实现的一个方法,童颜应该在合适的时候产生 KeyError 和 TypeError 异常。
  • __iter__(self, key)
    它应该返回当前容器的一个迭代器。迭代器以一连串内容的形式返回,最常见的是使用 iter() 函数调用,以及在类似 for x in container 的循环中被调用。迭代器是他们自己的队形,需要定义 __iter__ 方法并在其中返回自己。
  • __reversed__(self)
    定义了对容器使用 reversed() 内建函数时的行为。它应该返回一个反转之后的序列。当你的序列类是有序时,类似列表和元组,再实现这个方法。
  • __contains__(self, item)
    __contains__ 定义了使用 innot in 进行成员测试时类的行为。这个方法不是序列协议的一部分,原因是,如果 __contains__ 没有定义,Python 就会迭代整个序列,如果找到了需要的一项就返回 Ture。
  • __missing__(self, key)
    __missing__ 在字典的子类中使用,他定义了当试图方位一个字典中不存在的键时的行为(目前为止是指字典的实例,例如我有一个字典 d , 'george' 不是字典中的一个键,当试图访问 d['george'] 时就会调用 d.__missing__('george'))。

例子

一个实现了一些函数式结构的列表

class FunctionalList:
    ''' 一个列表的封装类,实现了一些额外的函数式方法
    例如 head, tail, init, last, drop 和 take
    '''
    def __init__(self, values=None):
        if values if 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 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]

当然,自定义序列有更大的用处,而且绝大部分都在标准库中实现了(Python 都是自带库的),像 Counter, OrderedDict 和 NamedTuple。

反射 Reflection

可以通过定义魔法方法来控制用于反射的内建函数 isinstanceissubclass 的行为。

  • __instancecheck__(self, instance)
    检查一个实例是否是你定义的类的一个实例(例如 isinstance(instance, class))。
  • __subclasscheck__(self, subclass)
    检查一个类是否是你定义的类的子类(例如 insubclass(subclass, class))。

这几个魔法方法的使用范围有些窄。相比其他魔法方法他们显得不是很重要。但是他们展示了在 Python 中进行面向对象编程时很重要的一点:不管做什么事情,都会有一个简单方法,不管它用不常用。这些魔法方法可能看起来没那么有用,但是当你正正需要用到他们的时候,你会感到幸运,因为他们还在那儿。

抽象基类

请参考 http://docs.python.org/2/library/abc.html

可调合的对象

在 Python 中,函数时一等的对象 (functions are first-class objects)。这意味着他们可以像其他任何对象一样被传递到函数和方法中,这是一个十分强大的特性。

Python 中一个特殊的魔法方法允许你自己类的对象表现得像是函数,然后你就可以“调用”他们,把它们传递到使用函数做参数的函数中,等等。这是另一个强大而且方便的特性,让使用 Python 编程变得更加幸福。

  • __call__(self, [args...])
    允许类的一个实例像函数那样被调用。本质上代表了 x()x.__call__() 是相同。注意,__call__ 可以有多个参数,这代表你可以像定义其他任何函数一样,定义 __call__,喜欢用多少参数就用多少。

    __call__ 在某些需要经常改变状态的类的实例中显得特别有用。“调用”这个实例来改变他的状态,是一种更加符合直觉,也更加优雅的方法。一个表示平面上实体的类是一个不错的例子:

class Entity:
    ''' 表示一个实体的类,调用它的实例可以更新实体的位置 entity's position'''
    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size

    def __call__(self, x, y):
        '''改变实体的位置'''
        self.x, self.y = x, y

上下文管理器 Context Manager

在 Python 2.5 中引入了一个全新的关键词,随之而来的是一种新的代码复用方法 -- with 声明。上下文管理的概念在 Python 中并不是全新引入的(之前他作为标准库的一部分实现),直到 PEP 343 被接受,它才成为一种一级的语言结构。可能你已经见过这种写法了:

with open('foo.txt') as bar:
    # 使用 bar 进行某些操作

当对象使用 with 声明创建时,上下文管理器允许类做一些设置和清理工作。上下文管理器的行为有下面两个魔方方法所定义:

  • __enter__(self)
    定义使用 with 声明创建的语句块最开始上下文管理器应该做些什么。注意__enter__ 的返回值会赋给 with 声明的目标,也就是 as 之后的东西。
  • __exit__(self, exception_type, exception_value, traceback)
    定义当 with 声明语句块执行完毕(或终止)时上下文管理器的行为。它可以用来处理异常,进行清理,或者做其他应该在语句块结束之后立刻执行的工作。如果语句块顺利执行,exception_typeexception_valuetraceback 会是 None。否则,你可以选择处理这个异常或者让用户来处理。如果你想处理异常,确保 __exit__ 在完成工作之后返回 True。如果你不想处理异常,那就让它发生吧。

对一些具有良好定义的且通用的设置和清理行为的类,__enter____exit__ 会变得特别有用。你也可以使用这几个方法来创建通用的上下文管理器,用来包装其他对象。下面是一个例子:

class Closer():
    """一个上下文管理器,可以在 with 语句中
    使用 close() 自动关闭对象"""

    def __init__(self, obj):
        self.obj = obj
    
    def __enter__(self, obj):
        return self.obj  # 绑定到目标

    def __exit__(self, exception_type, exception_value, traceback):
        try:
            self.obj.close()
        except AttributeError:  # obj 不是可关闭的
            print 'Not cloaseable.'
            return True

这是一个 Closer 在实际使用中的例子,使用一个 FTP 连接演示(一个可关闭的 socket):

>>> from magicmethods import Closer
>>> from ftplib import FTP
>>> with Closer(FTP('ftp.somesite.com')) as conn:
...     conn.dir()
... 
# 为了简单,省略了某些输出

>>> conn.dir()
... # 很长的 AttributeError 信息,不能使用一个已关闭的连接

>>> with Closer(int(5)) as i:
...     i += 1
...
Not closable.

>>> i
6

看到我们的包装器是如何同时优雅地处理正确和不正确的调用了吗?这就是上下文管理器和魔法方法的力量。Python 标准库包含一个 contextlib 模块,里面有一个上下文管理器 contextlib.closing() 基本上和我们的包装器完成的是同样的事情(但是没有包含任何当对象没有 close() 方法时的处理)。

创建描述符对象 Building Descriptor Objects

描述符 Descriptor 是一个类,当使用取值,赋值,和删除时它可以改变其它对象。描述符不是用来单独使用的,它们需要被一个拥有着类所包含(they're meant to be held by an owner class)。描述符可以用来创建面向对象数据库,以及创建某些属性之间互相依赖的类。描述符在表现具有不同单位的属性,或者需要计算的属性时显得特别有用(例如表现一个坐标系中的点的类,其中的距离原点的距离这种属性)。

要想成为一个描述符,一个类必须具有实现 __get__, __set__,和 __delete__ 三个方法中至少一个。

  • __get__(self, instance, owner)
    定义当试图取出描述符的值时的行为。instance 是拥有者类的实例,owner 是拥有者类本身。
  • __set__(self, instance, value)
    定义当描述符的值改变时的行为。instance 是拥有者类的实例,value 是要付给描述符的值。
  • __delete__(self, instance, owner)
    定义当描述符得知被删除时的行为。instance 是拥有者类的实例。

看一个描述符的有效应用,单位转换:

class Meter(object):
    ''' 米的描述符'''
    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):
    """英尺的描述符"""
    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()

>>> dis = Distance()
>>> dis.meter = 10
>>> dis.meter
10.0
>>> dis.foot
32.808

拷贝

当拷贝一个对象,改变这个对象而不影响原有的对象。这时就需要用到 Python 的 copy 模块了。Python 模块并不具有感知能力,我们需要告诉 Python 如何有效率的拷贝对象。

  • __copy__(self)
    定义对类的实例使用 copy.copy() 时的行为。copy.copy() 返回一个对象的浅拷贝,这意味着拷贝出的实例是全新的,然而里面的数据全都是引用的。也就是说,对象本身是拷贝的,但是它的数据还是引用的(所以浅拷贝中的数据更改会影响原对象)。
  • __deepcopy__(self, memodict)
    定义对类的实例使用 copy.deepcopy() 时的行为。copy.deepcopy() 返回一个对象的深拷贝,这个对象和他的数据全都被拷贝了一份。memodict 是一个先前拷贝对象的缓存,它优化了拷贝过程,而且可以防止拷贝递归数据结构是产生无限递归。当你想深拷贝一个单独的属性时,在那个属性上调用 copy.deepcopy(),使用 memodict 作为第一个参数。

这些魔法方法有什么用呢?像往常一样,当你需要比默认行为更加精确的控制时。

Pickling

Pickling 是 Python 数据结构的序列化过程,当你想存储一个对象稍后再取出读取时,Pickling 会显得十分有用。然而它同样也是担忧和混淆的主要来源。

Pickling 是如此的重要,以至于它不仅仅有自己的模块(pickle),还有自己的协议和魔法方法。首先,我们先来简要的介绍一下如何 pickle 已存在的对象类型。

Pickling:A Quick Soak in the Brine

假设你有一个字典,你想存储它,稍后再取出来。你可以把它的内容写入一个文件,小心翼翼地确保使用了正确地格式,要把它读取出来,你可以使用 exec() 或处理文件输入。但是这种方法并不可靠:如果你使用纯文本来存储重要数据,数据很容易以多种方式被破坏或者修改,导致你的程序崩溃,更糟糕的情况下,还可能在你的计算机上运行恶意代码。因此,我们要 pickle 它:

import pickle

data = {
    'foo': [1, 2, 3],
    'bar': ('Hello', 'world'),
    'baz': True
}

jar = open('data.pkl', 'wb')
pickle.dump(data. jar)  # 将 pickle 后的数据写入 jar 文件
jar.close()

过了几个小时,我们想把它取出来,我们只需要反 pickle 它:

import pickle
pkl_file = open('data.pkl', 'rb')  # 与 pickle 后的数据连接
data = pickle.load(pkl_file)  # 把它加载进一个变量
print data
pkl_file.close()

它就是我们之前的 data 。

现在,还需要谨慎地说一句: pickle 并不完美。Pickle 文件很容易因为事故或被故意的破坏掉。Pickling 或许比纯文本文件安全一些,但是依然有可能被用来运行恶意代码。而且它还不支持跨 Python 版本,所以不要指望分发pickle对象之后所有人都能正确地读取。然而不管怎么样,它依然是一个强有力的工具,可以用于缓存和其他类型的持久化工作。

Pickle 你的对象

Pickle 不仅仅可以用于内建类型,任何遵守 pickle 协议的类都可以被 pickle。Pickle 协议有四个可选方法,可以让类自定义他们的行为。

  • __getinitargs__(self)
    如果你想让你的类在反 pickle 时调用 __init__ ,你可以定义 __getinitargs__(self) ,它会返回一个参数元组,这个元组会传递给 __init__ 。注意,这个方法只能用于旧式类。
  • __getnewargs__(self)
    对新式类来说,可以通过这个方法改变类在 pickle 时传递给 __new__ 的参数。这个方法应该返回一个参数元组。
  • __getstate__(self)
    可以自定义对象被 pickle 时被存储的状态,而不是用对象的 __dict__ 属性。这个状态在对象被反 pickle 时会被 __setstate__ 使用。
  • __setstate__(self)
    当一个对象被反 pickle 时,如果定义了 __setstate__,对象的状态会传递给这个魔法方法,而不是直接应用到对象的 __dict__ 属性。这个魔法方法和 __get__ 相互依存:当这两个方法都被定义时,你可以在 pickle 时使用任何方法保存对象的任何状态。
  • __reduce__(self)
    当定义扩展类型时(也就是使用 Python 的 C 语言 API 实现的类型),如果你想 pickle 它们,你必须告诉 Python 如何 pickle 它们。__reduce__ 被定义之后,当对象被 pickle 时就会被调用。他要么返回一个代表全局名称的字符串,Python 会查找它并 pickle,要么返回一个元组。这个元组包含 2 到 5 个元素,其中包括一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 __setstate__ 的状态(可选);一个产生被 pickle 的列表元素的迭代器(可选);一个产生被 pickle 的字典元素的迭代器(可选);
  • __reduce_ex__(self)
    __reduce_ex__ 的存在是为了兼容性。如果它被定义,在 pickle 时 __reduce_ex__ 会代替 __reduce__ 被调用。 __reduce__ 也可以被定义,用于不支持 __reduce_ex__ 的旧版 pickle 的 API 调用。

一个例子

例子是 Slate,它会记住它的值曾经是什么,以及那些值是什么时候赋给他的。然而每次被 pickle 时它都会变成空白,因为当前的值不会被存储:

import time

class Slate:
    '''存储一个字符串和一个变更日志的类
    每次被 pickle 都会忘记它当前的值
    '''

    def __init__(self, value):
        self.value = value
        self.last_change = time.asctime()
        self.history = {}
    
    def change(self, new_value):
        # 改变当前值,将上一个值就到历史
        self.history[self.last_change] = self.value
        self.value = new_value
        self.last_change = time.asctime()

    def print_change(self):
        print('Change log for Slate object:')
        for k, v in self.history.items():
            print('{}\t{}'.format()k, v)

    def __getstate__(self):
        # 故意不返回 self.value 或 self.last_change
        # 我们想在反 pickle 时得到一个空白的 slate
        return self.history

    def __setstate__(self):
        # 使self.history = slate
        # value 和 last_change 为定义
        self.history = state
        self.value, self.last_change = None, None

总结

这本指南的目标是使所有阅读它的人都能有所收获,无论他们有没有使用 Python 或者进行面向对象编程的经验。如果你刚刚开始学习Python,你会得到宝贵的基础知识,了解如何写出具有丰富特性的,优雅而且易用的类。如果你是中级的 Python 程序员,你或许能掌握一些新的概念和技巧,以及一些可以减少代码行数的好办法。如果你是专家级别的 Python 爱好者,你又重新复习了一遍某些可能已经忘掉的知识,也可能顺便了解了一些新技巧。无论你的水平怎样,我希望这趟遨游 Python 特殊方法的旅行,真的对你产生了魔法般的效果(实在忍不住不说最后这个双关)。

附录1:如何调用魔法方法

一些魔法方法直接和内建函数对应,这种情况下,如何调用它们是显而易见的。然而,另外的情况下,调用魔法方法的途径并不是那么明显。这个附录旨在展示那些不那么明显的调用魔法方法的语法。

魔法方法 什么时候被调用 解释
__new__(cls [,...]) instance = MaClass(arg1, arg2) __new__ 在实例创建时调用
__init__(self[,...]) instance = MyClass(arg1, arg2) __init__ 在实例创建时调用
__cmp__(self) self == other, self > other 进行比较时调用
__pos__(self) +self 一元加法符号
__neg__(self) -self 一元减法符号
__invert__(self) ~self 按位取反
__index__(self) x[self] 当对象用于索引时
__nonzero__(self) bool(self 对象的布尔值
__getattr__(self, name) self.name # name 不存在 访问不存在的属性
__setattr__(self, name) self.name = val 给属性赋值
__delattr__(self, name) def self.name 删除属性
__getattribute(self, name) self.name 访问任意属性
__getitem__(self, key) self[key] 使用索引给某个元素赋值
__setitem__(self, key) self[key] = val 使用索引给某个元素赋值
__delitem__(self, key) del self[key] 使用索引删除某个对象
iter(self) for x in self 迭代
__contains__(self, value) value in self, value not in self 使用 in 进行成员测试
__call__(self [,...] self(args) “调用”一个实例
__enter__(self) with self as x: with 声明的上下文管理器
__exit__(self, exc, val, trace) with self as x with 声明的上下文管理器
__getstate__(self) pickle.dump(pkl_file, self) Pickling
__setstate(self) data = pickle.load(pkl_file) Pickling

Python 3 中的变化

记录几个在对象模型方面 Python 3 和 Python 2.x 之间的主要区别。

Python 3 中 string 和 unicode 的区别不复存在,因此 __unicode__ 被取消了, __bytes__ 加入进来(与 Python 2.7 中的 __str____unicode__ 行为类似),用于新的创建字节数组的内建方法。

Python 3 中默认除法变成了 true 除法,因此 __div__ 被取消了。
__coerce__ 被取消了,因为和其他魔法方法有功能上的重复,以及本身行为令人迷惑。
__cmp__ 被取消了,因为和其他魔法方法有功能上的重复。
__nonzero__ 被重命名成 __bool__

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容