Python的神奇方法指南:使操作符在自定义类内工作

使用 Python 神奇方法的优势之一就是它提供了一种简单的方式能让对象的行为像内建类型。这意味着你可以避免用丑陋,反直觉和非标准方法执行基本运算。在某些语言中,通常会这样做:

if instance.equals(other_instance):
    # do something

你也应该在 Python 确实会这样做,但同时它会增加用户的疑惑以及不必要的冗长。不同的库可能会对相同的运算采用不同的命名,这使得用户比平常干了更多的事。依靠神奇方法的力量,你可以定义一个方法(比如 __eq__),然后带代替我们真实的意图:

if instance == other_instance:
    # do something

现在你看到的是神奇方法力量的一部分。绝大多数都允许我们定义为运算符本身的意义,当用在我们自己定义的类上就像它们是内建类型。

神奇方法——比较

Python 有一整套神奇方法被设计用来通过操作符实现对象间直观的比较,而非别扭的方法调用。它们同样提供了一套覆盖 Python 对象比较的默认行为(通过引用)。以下是这些方法的列表以及做法:

__cmp__(self, other)

__cmp__是神奇方法中最基础的一个。实际上它实现所有比较操作符行为(<,==,!=,等),但它有可能不按你想要的方法工作(例如,一个实例是否等于另一个这取决于比较的准则,以及一个实例是否大于其他的这也取决于其他的准则)。如果 self < other,那 __cmp__ 应当返回一个负整数;如果 self == other,则返回 0;如果 self > other,则返回正整数。它通常是最好的定义,而不需要你一次就全定义好它们,但当你需要用类似的准则进行所有的比较时,__cmp__ 会是一个很好的方式,帮你节省重复性和提高明确度。

__eq__(self, other)

定义了相等操作符,==的行为。

__ne__(self, other)

定义了不相等操作符,!= 的行为。

__lt__(self, other)

定义了小于操作符,< 的行为。

__gt__(self, other)

定义了大于操作符,> 的行为。

__le__(self, other)

定义了小于等于操作符,<=的行为。

__ge__(self, other)

定义了大于等于操作符,>= 的行为。

举一个例子,设想对单词进行类定义。我们可能希望能够按内部对 string 的默认比较行为,即字典序(通过字母)来比较单词,也希望能够基于某些其他的准则,像是长度或音节数。在本例中,我们通过单词长度排序,以下给出实现:

class Word(str):
    '''单词类,比较定义是基于单词长度的'''

    def __new__(cls, word):
        # 注意,我们使用了__new__,这是因为str是一个不可变类型,
        # 所以我们必须更早地初始化它(在创建时)
        if ' ' in word:
            print "单词内含有空格,截断到第一部分"
            word = word[:word.index(' ')] # 在出现第一个空格之前全是字符了现在
        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)

现在,我们可以创建 2 个单词(通过 Word('foo') 和 Word('bar'))并基于它们的长度进行比较了。注意,我们没有定义 __eq__ 和 __ne__。这是因为这可能导致某些怪异的行为(特别是当比较 Word('foo') == Word('bar') 将会得到 True 的结果)。基于单词长度的相等比较会令人摸不清头脑,因此我们就沿用了str 本身的相等比较的实现。

现在可能是一个好时机来提醒你一下,你不必重载每一个比较相关的神奇方法来获得各种比较。标准库已经友好地为我们在模板 functools 中提供了一个装饰(decorator)类,定义了所有比较方法。你可以只重载 __eq__ 和一个其他的方法(比如 __gt__,__lt__,等)。这个特性只在 Python2.7(后?)适用,但当你有机会的话应该尝试一下,它会为你省下大量的时间和麻烦。你可以通过在你自己的重载方法在加上 @total_ordering 来使用。

神奇方法——数字

就像你可以通过重载比较操作符的途径来创建你自己的类实例,你同样可以重载数字操作符。系好你们的安全带,朋友们,还有很多呢。处于本文组织的需要,我会把数字的神奇方法分割成5块:一元操作符,常规算术操作符,反射算术操作符,增量赋值,类型转换。

一元操作符

一元运算和函数仅有一个操作数,比如负数,绝对值等

__pos__(self)

实现一元正数的行为(如:+some_object)

__neg__(self)

实现负数的行为(如: -some_object)

__abs__(self)

实现内建 abs() 函数的行为

__invert__(self)

实现用~操作符进行的取反行为。你可以参考 Wiki:bitwise operations 来解释这个运算符究竟会干什么

常规算术操作符

现在我们涵盖了基本的二元运算符:+,-,* 等等。其中大部分都是不言自明的。

__add__(self, other)

实现加法

__sub__(self, other)

实现减法

__mul__(self, other)

实现乘法

__floordiv__(self, other)

实现地板除法,使用 // 操作符

__div__(self, other)

实现传统除法,使用 / 操作符

__truediv__(self, other)

实现真正除法。注意,只有当你 from __future__ import division 时才会有效

__mod__(self, other)

实现求模,使用 % 操作符

__divmod__(self, other)

实现内建函数 divmod() 的行为

__pow__(self, other)

实现乘方,使用 ** 操作符

__lshift__(self, other)

实现左按位位移,使用 << 操作符

__rshift__(self, other)

实现右按位位移,使用 >> 操作符

__and__(self, other)

实现按位与,使用 & 操作符

__or__(self, other)

实现按位或,使用 | 操作符

__xor__(self, other)

实现按位异或,使用 ^ 操作符

反射算术操作符

你知道我会如何解释反射算术操作符?你们中的有些人或许会觉得它很大,很可怕,是国外的概念。但它实际上很简单,下面给一个例子:

some_object + other

这是“常规的”加法。而反射其实相当于一回事,除了操作数改变了改变下位置:

other + some_object

因此,所有这些神奇的方法会做同样的事等价于常规算术操作符,除了改变操作数的位置关系,比如第一个操作数和自身作为第二个。此外没有其他的操作方式。在大多数情况下,反射算术操作的结果等价于常规算术操作,所以你尽可以在刚重载完 __radd__就调用 __add__。干脆痛快:

__radd__(self, other)

实现反射加法

__rsub__(self, other)

实现反射减法

__rmul__(self, other)

实现反射乘法

__rfloordiv__(self, other)

实现反射地板除,用 // 操作符

__rdiv__(self, other)

实现传统除法,用 / 操作符

__rturediv__(self, other)

实现真实除法,注意,只有当你 from __future__ import division 时才会有效

__rmod__(self, other)

实现反射求模,用 % 操作符

__rdivmod__(self, other)

实现内置函数 divmod() 的长除行为,当调用 divmod(other,self) 时被调用

__rpow__(self, other)

实现反射乘方,用 ** 操作符

__rlshift__(self, other)

实现反射的左按位位移,使用 << 操作符

__rrshift__(self, other)

实现反射的右按位位移,使用 >> 操作符

__rand__(self, other)

实现反射的按位与,使用 & 操作符

__ror__(self, other)

实现反射的按位或,使用 | 操作符

__rxor__(self, other)

实现反射的按位异或,使用 ^ 操作符

增量赋值

Python 也有各种各样的神奇方法允许用户自定义增量赋值行为。你可能已经熟悉增量赋值,它结合了“常规的”操作符和赋值。如果你仍不明白我在说什么,下面有一个例子:

x = 5
x += 1 # 等价 x = x + 1

这些方法都不会有返回值,因为赋值在 Python 中不会有任何返回值。反而它们只是改变类的状态。列表如下:

__iadd__(self, other)

实现加法和赋值

__isub__(self, other)

实现减法和赋值

__imul__(self, other)

实现乘法和赋值

__ifloordiv__(self, other)

实现地板除和赋值,用 //= 操作符

__idiv__(self, other)

实现传统除法和赋值,用 /= 操作符

__iturediv__(self, other)

实现真实除法和赋值,注意,只有当你 from __future__ import division 时才会有效

__imod__(self, other)

实现求模和赋值,用 %= 操作符

__ipow__(self, other)

实现乘方和赋值,用 **= 操作符

__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)

实现到复数的类型转换

__oct__(self)

实现到 8 进制的类型转换

__hex__(self)

实现到 16 进制的类型转换

__index__(self)

实现一个当对象被切片到 int 的类型转换。如果你自定义了一个数值类型,考虑到它可能被切片,所以你应该重载__index__

__trunc__(self)

当 math.trunc(self) 被调用时调用。__trunc__ 应当返回一个整型的截断,(通常是 long)

__coerce__(self, other)

该方法用来实现混合模式的算术。如果类型转换不可能那 __coerce__ 应当返回 None。 否则,它应当返回一对包含 self 和 other(2 元组),且调整到具有相同的类型

相关的文章:

暂无评论

写评论