Skip to content

自定义类

使用 Python 的魔法方法的最大优势之一是它们提供了一种简单的方法来使对象表现得像内置类型。这意味着您可以避免执行基本运算符的丑陋、违反直觉和非标准的方式。在某些语言中,通常会执行以下操作:

python
if instance.equals(other_instance):
    # do something

您当然也可以在 Python 中执行此操作,但这会增加混乱并且不必要地冗长。不同的库可能对相同的操作使用不同的名称,这使得客户端做的工作比必要的多。然而,借助魔法方法的力量,我们可以定义一种方法(__eq__ 在本例中为 ),然后说出我们的意思:

python
if instance == other_instance:
    # do something

这就是魔法方法力量的一部分。它们中的绝大多数允许我们为运算符定义含义,以便我们可以在我们自己的类中使用它们,就像它们是在类型中构建的一样。

比较魔术方法

Python 有一大堆神奇的方法,旨在使用运算符实现对象之间的直观比较,而不是笨拙的方法调用。它们还提供了一种覆盖对象比较的默认 Python 行为的方法(通过引用)。以下是这些方法的列表以及它们的作用:

方法名作用
__eq__(self, other)定义相等运算符的行为,==
__ne__(self, other)定义不等式运算符 的行为!=
__lt__(self, other)定义小于运算符 的行为<
__gt__(self, other)定义大于运算符 的行为>
__le__(self, other)定义小于或等于运算符 的行为<=
__ge__(self, other)定义大于或等于运算符 的行为>=

例如,考虑一个类来模拟一个单词。我们可能希望按字典顺序(按字母表)比较单词,这是字符串的默认比较行为,但我们也可能希望根据其他一些标准来进行比较,例如长度或音节数。在本例中,我们将按长度进行比较。这是一个实现:

python
class Word(str):
    """Class for words, defining comparison based on word length."""

    def __new__(cls, word):
        # Note that we have to use __new__. This is because str is an immutable
        # type, so we have to initialize it early (at creation)
        if ' ' in word:
            print("Value contains spaces. Truncating to first space.")
            word = word[:word.index(' ')]  # Word is now all chars before first space
        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')and Word('bar'))并根据长度比较它们。但是请注意,我们没有定义__eq__ and __ne__。这是因为这会导致一些奇怪的行为(特别是 Word('foo') == Word('bar') 会评估为真)。根据长度来测试相等性是没有意义的,所以我们依靠 str ' 实现相等性。

比较运算符

下面是两条基本的规则: 首先,运算符的实现基于左操作数:A < B 相当于A.__lt__(B) 。 其次,相反的运算符的实现基于右操作数:A < B 相当于B.__gt__(A) 。 如果右操作数是左操作数的一个子类,那这样的比较基本不会有什么异常发生;同时,Python会首先检测右操作数,以确保这个子类可以重载基类。

下面,我们通过一个例子看看这两条规则是如何工作的,我们定义了一个只包含其中一个运算符实现的类,然后把这个类用于另外一种操作。

下面是我们使用类中的一段代码。

python
class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def __gt__(self, other):
        # other 是另外一个对象
        return self.area() < self.area()

这段代码基于 RectAngle(矩形) 的比较规则,主要是对比三角形的大小。

我们省略了比较方法,看看当缺少比较运算符时,Python将如何回退。这个类允许我们进行 < 比较。但是有趣的是,通过改变操作数的顺序,Python也可以使用这个类进行 > 比较。换句话说,x < yy >x 是等价的。这遵从了镜像反射法则;

当我们试图评估不同的比较运算时就会看到这种现象。下面,我们创建两个 RectAngle 类,然后用不同的方式比较它们。

python
>>> r1 = RectAngle(4, 5)
>>> r2 = RectAngle(5, 6)
>>> r1 > r2
True
>>> r1 < r2
False
>>> r1 == r2
False

从代码中,我们可以看到,r1 < r2 调用了r1.__lt__(three)

但是,对于r1 > three ,由于没有定义__gt__() ,Python使用r2.__lt__(two) 作为备用的比较方法。

默认情况下,__eq__() 方法从object 继承而来,它比较不同对象的ID值。当我们用于 ==!= 比较对象时,结果如下。

python
>>> r1_2 = RectAngle(4, 5)
>>> r1_2 == r1
False

可以看到,结果和我们预期的不同。所以,我们通常都会需要重载默认的__eq__() 实现。

此外,逻辑上,不同的运算符之间是没有联系的。但是从数学的角度来看,我们可以基于两个运算符完成所有必需的比较运算。Python 没有实现这种机制。相反,Python 默认认为下面的 4 组比较是等价的。

x < yy > x

xyyx

x = yy = x

xyyx

这意味着,我们必须至少提供每组中的一个运算符。例如,我可以提供 __eq__()__ne__()__lt__()__le__() 的实现。

当设计比较运算符时,要考虑两个因素。

  • 如何比较同一个类的两个对象。
  • 如何比较不同类的对象。

对于一个有许多属性的类,当我们研究它的比较运算符时,通常会觉得有很明显的歧义。或许这些比较运算符的行为和我们的预期不完全相同。

同类比较

下面我们通过一个更完整的 RectAngle 类来看一下简单的同类比较。

python
class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def __lt__(self, other):
        return self.area() > other.area()

    def __le__(self, other):
        return self.area() <= other.area()

    def __gt__(self, other):
        # other 是另外一个对象
        return self.area() < other.area()

    def __ge__(self, other):
        return self.area() >= other.area()

    def __eq__(self, other):
        return self.area() == other.area()

    def __ne__(self, other):
        return self.area() != other.area()


r1 = RectAngle(4, 5)
r2 = RectAngle(5, 6)

print(r1 > r2)
print(r1 < r2)

现在我们定义了 6 个比较运算符。

我们也没有给出类内比较的代码,这个我们会在下一个部分中详细讲解。用上面定义的这个类,我们可以成功地比较不同的牌。下面是一个创建并比较3张牌的例子。

python
>>> r1 = RectAngle(4, 5)
>>> r2 = RectAngle(5, 6)
>>> r3 = RectAngle(4, 6)

用上面定义的RectAngle 类,我们可以进行像下面这样的一系列比较。

python
>>> r1 == r2
False
>>> r1.width == r3.width
True
>>> r1 < r3
False

这个类的行为与我们预期的一致。

不同类比较

我们会继续以RectAngle 类为例来看看当两个比较运算中的两个操作数属于不同的类时会发生什么。

新增一个三角形对象

python
class Triangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height / 2

    def __lt__(self, other):
        return self.area() > other.area()

    def __eq__(self, other):
        return self.area() == other.area()

下面我们将一个RectAngle 实例和一个Triangle 值进行比较。

python
>>> r1 = RectAngle(4, 5)
>>> t1 = Triangle(4, 5)
>>> r1 == t1
>>> r1 + t1
Traceback(most recent call last): 
  File "<stdin>", lin1, in <module>
TypeError: unsupported operand type(s) for +: 'RectAngle' and 'Triangle'
>>>

可以看到,这和我们预期的行为一致,RectAngle 的子类Triangle 没有实现必需的特殊方法,所以产生了一个TypeError 异常。

数值魔术方法

就像您可以为类的实例创建与比较运算符进行比较的方法一样,您可以定义数字运算符的行为。扣好你的安全带,伙计们......有很多这样的。为了组织起见,我将数值魔术方法分为 5 类:一元运算符、普通算术运算符、反射算术运算符(稍后会详细介绍)、增强赋值和类型转换。

一元运算符和函数

一元运算符和函数只有一个操作数,例如求反、绝对值等。

方法名作用
__pos__(self)实现一元正数的行为(例如+some_object
__neg__(self)实现否定行为(例如-some_object
__abs__(self)实现内置abs()函数的行为。
__invert__(self)使用运算符实现反转行为~
__round__(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)实现真正的除法。请注意,这仅 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)使用运算符实现按位异或 ^

接下来实现一个圆的四则运算

python
import math


class Circle:

    def __init__(self, r):
        self.r = r

    @property
    def area(self):
        return math.pi * self.r ** 2

    def __add__(self, other):
        new_area = self.area + other.area
        print(new_area)
        new_r = math.sqrt(new_area / math.pi)
        return Circle(new_r)


c1 = Circle(3)
c2 = Circle(4)
c3 = c1 + c2
print(c3, c3.r)

反射算术运算符

你知道我怎么说我会稍微反射算术吗?你们中的一些人可能会认为这是一些大而可怕的外国概念。其实很简单。这是一个例子:

python
some_object + other

那是“正常”的加法。反射的等价物是一样的,除了操作数交换了:

python
other + some_object

因此,所有这些魔术方法都与它们的正常等效方法做同样的事情,除了以 other 作为第一个操作数和 self 作为第二个操作数执行操作,而不是相反。在大多数情况下,反射操作的结果与其正常等效的结果相同,因此您最终可能只是定义__radd__ 为调用__add__等。请注意,运算符左侧的对象(other在示例中)不得定义(或 return NotImplemented )其对操作的非反射版本的定义。例如,在示例中,只有在没有定义some_object.__radd__时才会调用。other.__add__

方法名作用
__radd__(self, other)实现反射加法。
__rsub__(self, other)实现反射减法。
__rmul__(self, other)实现反射乘法。
__rfloordiv__(self, other)使用运算符实现反射整数除法//
__rdiv__(self, other)使用运算符实现反射除法/
__rtruediv__(self, other)工具反映了真正的除法。请注意,这仅from __future__ import division在生效时才有效。
__rmod__(self, other)使用运算符实现反射模数%
__rdivmod__(self, other)当被调用divmod()时,使用内置函数实现长除法的行为。divmod(other, self)
__rpow__使用运算符实现反射指数的行为**
__rlshift__(self, other)使用运算符实现反射左位移位<<
__rrshift__(self, other)使用运算符实现反射右位移位>>
__rand__(self, other)实现按位反射并使用&运算符。
__ror__(self, other)实现按位反射或使用`
__rxor__(self, other)使用运算符实现反射位异或^

增强分配

Python 还具有多种神奇的方法,允许为增强赋值定义自定义行为。您可能已经熟悉增强赋值,它结合了“普通”运算符和赋值。如果你仍然不知道我在说什么,这里有一个例子:

python
x = 5
x += 1 # in other words x = x + 1

这些方法中的每一个都应返回应分配给左侧变量的值(例如, for a += b__iadd__可能返回a + b,将分配给a)。这是列表:

方法名作用
__iadd__(self, other)通过赋值实现加法。
__isub__(self, other)用赋值实现减法。
__imul__(self, other)用赋值实现乘法。
__ifloordiv__(self, other)使用//=运算符通过赋值实现整数除法。
__idiv__(self, other)使用/=运算符通过赋值实现除法。
__itruediv__(self, other)通过赋值实现真正的除法。请注意,这仅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)实现类型转换为浮点数。
__complex__(self)实现复杂的类型转换。
__oct__(self)实现类型转换为八进制。
__hex__(self)实现到十六进制的类型转换。
__index__(self)当对象在切片表达式中使用时,实现到 int 的类型转换。如果您定义了可能在切片中使用的自定义数字类型,您应该定义__index__.
__trunc__(self)调用时math.trunc(self)调用。__trunc__应该将 `self 截断为整数类型(通常是长整数)的值返回。

表示类的魔法方法

拥有一个类的字符串表示通常很有用。在 Python 中,您可以在类定义中实现一些方法来自定义返回类表示的内置函数的行为方式。

方法名作用
__str__(self)定义何时str()在您的类的实例上调用的行为。
__repr__(self)定义何时repr()在您的类的实例上调用的行为。str()和之间的主要区别repr()是目标受众。repr()旨在产生主要是机器可读的输出(在许多情况下,它甚至可能是有效的 Python 代码),而str()旨在是人类可读的。
__unicode__(self)定义何时unicode()在您的类的实例上调用的行为。unicode()就像str(),但它返回一个 unicode 字符串。请注意:如果客户端调用str()您的类的实例而您只定义__unicode__()了 ,它将无法正常工作。您应该始终尝试定义__str__(),以防有人没有使用 unicode 的奢侈。
__format__(self, formatstr)定义在新样式字符串格式中使用类的实例时的行为。例如,"Hello, {0:abc}!".format(a)会导致 call a.__format__("abc")。这对于定义您自己可能希望提供特殊格式选项的数字或字符串类型很有用。
__hash__(self)定义何时hash()在您的类的实例上调用的行为。它必须返回一个整数,其结果用于字典中的快速键比较。请注意,这通常__eq__也需要实施。遵守以下规则:a == b暗示hash(a) == hash(b)
__nonzero__(self)定义何时bool()在您的类的实例上调用的行为。应该返回Trueor False,这取决于您是否要将实例视为Trueor False
__dir__(self)定义何时dir()在您的类的实例上调用的行为。此方法应返回用户的属性列表。通常,实现是不必要的,但如果您重新定义或(您将在下一节中看到)或以其他方式动态生成属性__dir__,那么它对于您的类的交互式使用至关重要。__getattr__ __getattribute__
__sizeof__(self)定义何时sys.getsizeof()在您的类的实例上调用的行为。这应该返回对象的大小,以字节为单位。这对于在 C 扩展中实现的 Python 类通常更有用,但了解它会有所帮助。

我们已经完成了魔法方法指南中无聊(且无示例)的部分。现在我们已经介绍了一些更基本的魔法方法,是时候转向更高级的材料了。

案例 __repr__()

__repr__()__str__() 方法

对于一个对象,Python提供了两种字符串表示。它们和内建函数repr()str()print()string.format() 的功能是一致的。

  • 通常,str() 方法表示的对象对用户更加友好。这个方法是由对象的__str__ 方法实现的。

  • repr() 方法的表示通常会更加技术化

这个方法是由__repr__() 方法实现的。

  • print() 函数会调用str() 来生成要输出的对象。
  • 字符串的format() 函数也可以使用这些方法。当我们使用{!r} 或者{!s} 格式时,我们实际上分别调用了__repr__() 或者__str__() 方法。

下面我们先来看一下这些方法的默认实现。

直接打印对象的实现方法,__str__ 是被print函数调用的,一般都是return一个什么东西,这个东西应该是以字符串的形式表现的。如果不是要用str() 函数转换,我们可以直接print的对象都是实现了`__str__这个方法的,比如dict。看下面的例子。

下面是一个很简单的类。

python
class RectAngle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def __str__(self):
        return f'<RectAngle: {self.width}(w) * {self.height}(h)>'

我们定义了两个简单类,每个类包含4个属性。

下面是在命令行中使用RectAngle 类的结果。

python
>>> rect = RectAngle(4, 5)
>>> print(rect)
<RectAngle: 4(w) * 5(h) >
>>> rect
<__main__.RectAngle object at 0x000002A1547E5A58>

可以看到,__str__() print 方法打印的内容就看起来更加输入,但是在命令行里面里面的调试信息还是现实的为对象,当增加 __repr__() 方法之后,调试信息也会变得更加清楚

python
def __repr__(self):
    return f'<RectAngle: w{self.width} h{self.height}>'

在以下两种情况下,我们可以考虑重写__str__()__repr__()

  • 非集合对象 :一个不包括任何其他集合对象的“简单”对象,这类对象的格式化通常不会特别复杂。
  • 集合对象 :一个包含集合的对象,这类对象的格式化会更为复杂。