美文网首页好玩的 Python
Python 自定义类中的函数和运算符重载

Python 自定义类中的函数和运算符重载

作者: 爱吃鱼de大猫 | 来源:发表于2018-11-01 15:28 被阅读16次

    原文:https://realpython.com/operator-function-overloading/

    如果你曾在字符串(str)对象上进行过 + 或 * 运算,你一定注意到它跟整数或浮点数对象的行为差异:

    >>> # 加法
    >>> 1 + 2
    3
    
    >>> # 拼接字符串
    >>> 'Real' + 'Python'
    'RealPython'
    
    
    >>> # 乘法
    >>> 3 * 2
    6
    
    >>> # 重复字符串
    >>> 'Python' * 3
    'PythonPythonPython'
    

    你可能想知道,为什么同一个内置的操作符或函数,作用在不同类的对象上面会展现出不同的行为。这种现象被称为运算符重载或者函数重载。本文将帮助你理解这个机制,今后你可以运用到你的自定义类中,让你的编码更 Pythonic 。

    以下你将学到:

    • Python 处理运算符和内置函数的API
    • len() 以及其它内置函数背后的“秘密”
    • 如何让你的类可以使用运算符进行运算
    • 如何让你的类与内置函数的操作保持兼容及行为一致

    此外,后面还将提供一个具体的类的实例。它的实例对象的行为与运算符及内置函数的行为保持一致。

    Python 数据模型

    假设,你有一个用来表示在线购物车的类,包含一个购物车(列表)和一名顾客(字符串或者其它表示顾客类的实例)。

    这种情形下,很自然地需要获取购物车的列表长度。Python 的新手可能会考虑在他的类中实现一个叫 get_cart_len() 的方法来处理这个需求。实际上,你只需要配置一下,当我们传入购物车实例对象时,使用内置函数 len() 就可以返回购物车的长度。

    另一个场景中,我们可能需要添加某些商品到购物车。某些新手同样会想要实现一个叫 append_to_cart() 的方法来处理获取一个项,并将它添加到购物车列表中。其实你只需配置一下 + 运算符就可以实现将项目添加到购物车列表的操作。

    Python 使用特定的方法来处理这些过程。这些特殊的方法都有一个特定的命名约定,以双下划线开始,后面跟命名标识符,最后以双下划线结束。

    本质上讲,每一种内置的函数或运算符都对应着对象的特定方法。比如,len()方法对应内置 len() 函数,而 add() 方法对应 + 运算符。

    默认情况下,绝大多数内置函数和运算符不会在你的类中工作。你需要在类定义中自己实现对应的特定方法,实例对象的行为才会和内置函数和运算符行为保持一致。当你完成这个过程,内置函数或运算符的操作才会如预期一样工作

    这些正是数据模型帮你完成的过程(文档的第3部分)。该文档中列举了所有可用的特定方法,并提供了重载它们的方法以便你在自己的对象中使用。

    我们看看这意味着什么。

    趣事:由于这些方法的特殊命名方式,它们又被称作 dunder 方法,是双下划线方法的简称。有时候它们也被称作特殊方法或魔术方法。我们更喜欢 dunder 方法这个叫法。

    len() 和 [] 的内部运行机制

    每一个 Python 类都为内置函数或运算符定义了自己的行为方式。当你将某个实例对象传入内置函数或使用运算符时,实际上等同于调用带相应参数的特定方法。

    如果有一个内置函数,func(),它关联的特定方法是 func(),Python 解释器解释为类似于 obj.func() 的函数调用,obj 就是实例对象。如果是运算符操作,比如 opr ,关联的特定方法是 opr(),Python 将 obj1 <opr> obj2 解释为类似于 obj1.opr(obj2) 的形式。
    所以,当你在实例对象上调用 len() 时,Python 将它处理为 obj.len() 调用。当你在可迭代对象上使用 [] 运算符来获取指定索引位置上的值时,Python 将它处理为 itr.getitem(index),itr 表示可迭代对象,index 表示你要索引的位置。

    因此,在你定义自己的类时,你可以重写关联的函数或运算符的行为。因为,Python 在后台调用的是你定义的方法。我们看个例子来理解这种机制:

    >>> a = 'Real Python'
    >>> b = ['Real', 'Python']
    >>> len(a)
    11
    >>> a.__len__()
    11
    >>> b[0]
    'Real'
    >>> b.__getitem__(0)
    'Real'
    

    如你所见,当你分别使用函数或者关联的特定方法时,你获得了同样的结果。实际上,如果你使用内置函数 dir() 列出一个字符串对象的所有方法和属性,你也可以在里面找到这些特定方法:

    >>> dir(a)
    ['__add__',
     '__class__',
     '__contains__',
     '__delattr__',
     '__dir__',
     ...,
     '__iter__',
     '__le__',
     '__len__',
     '__lt__',
     ...,
     'swapcase',
     'title',
     'translate',
     'upper',
     'zfill']
    

    如果内置函数或运算符的行为没有在类中特定方法中定义,你会得到一个类型错误。
    那么,如何在你的类中使用特定方法呢?

    重载内置函数

    数据模型中定义的大多数特定方法都可以用来改变 len, abs, hash, divmod 等内置函数的行为。你只需要在你的类中定义好关联的特定方法就好了。下面举几个栗子:

    用 len() 函数获取你对象的长度

    要更改 len() 的行为,你需要在你的类中定义 len() 这个特定方法。每次你传入类的实例对象给 len() 时,它都会通过你定义的 len() 来返回结果。下面,我们来实现前面 order 类的 len() 函数的行为:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __len__(self):
    ...         return len(self.cart)
    ...
    >>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
    >>> len(order)
    3
    

    如你所见,你现在可以直接使用 len() 来获得购物车列表长度。相比 order.get_cart_len() 调用方式,使用 len() 更符合“队列长度”这个直观表述,你的代码调用更 Pythonic,更符合直观习惯。如果你没有定义 len() 这个方法,当你调用 len() 时就会返回一个类型错误:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    >>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
    >>> len(order)  # Calling len when no __len__
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: object of type 'Order' has no len()
    

    此外,当你重载 len() 时,你需要记住的是 Python 需要该函数返回的是一个整数值,如果你的方法函数返回的是除整数外的其它值,也会报类型错误(TypeError)。此做法很可能是为了与 len() 通常用于获取序列的长度这种用途(序列的长度只能是整数)保持一致:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __len__(self):
    ...         return float(len(self.cart))  # Return type changed to float
    ...
    >>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
    >>> len(order)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'float' object cannot be interpreted as an integer
    

    让你的对象提供 abs() 运算

    你可以通过定义类的 abs() 方法来控制内置函数 abs() 作用于实例对象时的行为。abs() 函数对返回值没有约束,只是在你的类没有定义关联的特定方法时会得到类型错误。
    在表示二维空间向量的类中, abs() 函数可以被用来获取向量的长度。下面演示如何做:

    >>> class Vector:
    ...     def __init__(self, x_comp, y_comp):
    ...         self.x_comp = x_comp
    ...         self.y_comp = y_comp
    ...
    ...     def __abs__(self):
    ...         return (x * x + y * y) ** 0.5
    ...
    >>> vector = Vector(3, 4)
    >>> abs(vector)
    5.0
    

    这样表述为“向量的绝对值”相对于 vector.get_mag() 这样的调用会显得更直观。

    通过 str() 提供更加美观的对象输出格式

    内置函数 str() 通常用于将类实例转换为字符串对象,更准确地说,为普通用户提供更友好的字符串表示方式,而不仅仅是面向程序员。通过在你的类定义中实现 str() 特定方法你可以自定义你的对象使用 str() 输出时的字符串输出格式。此外,当你使用 print() 输出你的对象时 Python 实际上调用的也是 str() 方法。
    我们将在 Vector 类中实现 Vector 对象的输出格式为 xi+yj。负的 Y 方向的分量输出格式使用迷你语言来处理:

    >>> class Vector:
    ...     def __init__(self, x_comp, y_comp):
    ...         self.x_comp = x_comp
    ...         self.y_comp = y_comp
    ...
    ...     def __str__(self):
    ...         # By default, sign of +ve number is not displayed
    ...         # Using `+`, sign is always displayed
    ...         return f'{self.x_comp}i{self.y_comp:+}j'
    ...
    >>> vector = Vector(3, 4)
    >>> str(vector)
    '3i+4j'
    >>> print(vector)
    3i+4j
    

    需要注意的是 str() 必须返回一个字符串对象,如果我们返回值的类型为非字符串类型,将会报类型错误。

    使用 repr() 来显示你的对象

    repr() 内置函数通常用来获取对象的可解析字符串表示形式。如果一个对象是可解析的,这意味着使用 repr 再加上 eval() 此类函数,Python 就可以通过字符串表述来重建对象。要定义 repr() 函数的行为,你可以通过定义 repr() 方法来实现。

    这也是 Python 在 REPL(交互式)会话中显示一个对象所使用的方式 。如果 repr() 方法没有定义,你在 REPL 会话中试图输出一个对象时,会得到类似 <main.Vector object at 0x...> 这样的结果。我们来看 Vector 类这个例子的实际运行情况:

    >>> class Vector:
    ...     def __init__(self, x_comp, y_comp):
    ...         self.x_comp = x_comp
    ...         self.y_comp = y_comp
    ...
    ...     def __repr__(self):
    ...         return f'Vector({self.x_comp}, {self.y_comp})'
    ...
    
    >>> vector = Vector(3, 4)
    >>> repr(vector)
    'Vector(3, 4)'
    
    >>> b = eval(repr(vector))
    >>> type(b), b.x_comp, b.y_comp
    (__main__.Vector, 3, 4)
    
    >>> vector  # Looking at object; __repr__ used
    'Vector(3, 4)'
    

    注意:如果 str() 方法没有定义,当在对象上调用 str() 函数,Python 会使用 repr() 方法来代替,如果两者都没有定义,默认输出为 <main.Vector ...>。在交互环境中 repr() 是用来显示对象的唯一方式,类定义中缺少它,只会输出 <main.Vector ...>。
    尽管,这是官方推荐的两者行为的区别,但在很多流行的库中实际上都忽略了这种行为差异,而交替使用它们。
    关于 repr() 和 str() 的问题推荐阅读 Dan Bader 写的这篇比较出名的文章:Python 字符串转换 101:为什么每个类都需要定义一个 “repr”

    使用 bool() 提供布尔值判断

    内置的函数 bool() 可以用来提供真值检测,要定义它的行为,你可以通过定义 bool() (Python 2.x版是 nonzero())特定方法来实现。
    此处的定义将供所有需要判断真值的上下文(比如 if 语句)中使用。比如,前面定义的 Order 类,某个实例中可能需要判断购物车长度是否为非零。用来检测是否继续处理订单:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __bool__(self):
    ...         return len(self.cart) > 0
    ...
    >>> order1 = Order(['banana', 'apple', 'mango'], 'Real Python')
    >>> order2 = Order([], 'Python')
    
    >>> bool(order1)
    True
    >>> bool(order2)
    False
    
    >>> for order in [order1, order2]:
    ...     if order:
    ...         print(f"{order.customer}'s order is processing...")
    ...     else:
    ...         print(f"Empty order for customer {order.customer}")
    
    Real Python's order is processing...
    Empty order for customer Python
    

    注意:如果类的 bool() 特定方法没有定义, len() 方法返回值将会用来做真值判断,如果是一个非零值则为真,零值为假。如果两个方法都没有被定义,此类的所有实例检测都会被判断为真值。

    还有更多用来重载内置函数的特定方法,你可以在官方文档中找到它们的用法,下面我们开始讨论运算符重载的问题。

    重载内置运算符

    要改变一个运算符的行为跟改变函数的行为一样,很简单。你只需在类中定义好对应的特定方法,运算符就会按照你设定的方式运行。

    跟上面的特定方法不同的是,这些方法定义中,除了接收自身(self)这个参数外,它还需要另一个参数
    下面,我们看几个例子。

    让你的对象能够使用 + 运算符做加法运算

    与 + 运算符对应的特定方法是 add() 方法。添加一个自定义的 add() 方法将会改变该运算符的行为。建议让 add() 方法返回一个新的实例对象而不要修改调用的实例本身。在 Python 中,这种行为非常常见:

    >>> a = 'Real'
    >>> a + 'Python'  # Gives new str instance
    'RealPython'
    >>> a  # Values unchanged
    'Real'
    >>> a = a + 'Python'  # Creates new instance and assigns a to it
    >>> a
    'RealPython'
    

    你会发现上面例子中字符串对象进行 + 运算会返回一个新的字符串,原来的字符串本身并没有被改变。要改变这种方式,我们需要显式地将生成的新实例赋值给 a。

    我们将在 Order 类中实现通过 + 运算符来将新的项目添加到购物车中。我们遵循推荐的方法,运算后返回一个新的 Order 实例对象而不是直接更改现有实例对象的值:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __add__(self, other):
    ...         new_cart = self.cart.copy()
    ...         new_cart.append(other)
    ...         return Order(new_cart, self.customer)
    ...
    >>> order = Order(['banana', 'apple'], 'Real Python')
    
    >>> (order + 'orange').cart  # New Order instance
    ['banana', 'apple', 'mango']
    >>> order.cart  # Original instance unchanged
    ['banana', 'apple']
    
    >>> order = order + 'mango'  # Changing the original instance
    >>> order.cart
    ['banana', 'apple', 'mango']
    

    同样的,还有其他的 sub(), mul() 等等特定方法,它们分别对应 -*,等等运算符。它们也都是返回新的实例对象。

    一种快捷方式:+= 运算符

    += 运算符通常作为表达式 obj1 = obj1 + obj2 的一种快捷方式。对应的特定方法是 iadd(),该方法会直接修改自身的值,返回的结果可能是自身也可能不是自身。这一点跟 add() 方法有很大的区别,后者是生成新对象作为结果返回。

    大致来说,+= 运算符等价于:

    >>> result = obj1 + obj2
    >>> obj1 = result
    

    上面,result 是 iadd() 返回的值。第二步赋值是 Python 自动处理的,也就是说你无需显式地用表达式 obj1 = obj1 + obj2 将结果赋值给 obj1 。
    我们将在 Order 类中实现这个功能,这样我们就可以使用 += 来添加新项目到购物车中:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __iadd__(self, other):
    ...         self.cart.append(other)
    ...         return self
    ...
    >>> order = Order(['banana', 'apple'], 'Real Python')
    >>> order += 'mango'
    >>> order.cart
    ['banana', 'apple', 'mango']
    

    如上所见,所有的更改是直接作用在对象自身上,并返回自身。如果我们让它返回一些随机值比如字符串、整数怎样?

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __iadd__(self, other):
    ...         self.cart.append(other)
    ...         return 'Hey, I am string!'
    ...
    >>> order = Order(['banana', 'apple'], 'Real Python')
    >>> order += 'mango'
    >>> order
    'Hey, I am string!'
    

    尽管,我们往购物车里添加的是相关的项,但购物车的值却变成了 iadd() 返回的值。Python 在后台隐式处理这个过程。如果你在方法实现中忘记处理返回内容,可能会出现令人惊讶的行为:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __iadd__(self, other):
    ...         self.cart.append(other)
    ...
    >>> order = Order(['banana', 'apple'], 'Real Python')
    >>> order += 'mango'
    >>> order  # No output
    >>> type(order)
    NoneType
    

    Python 中所有的函数(方法)默认都是返回 None,因此,order 的值被设置为默认值 None,交互界面不会有输出显示。如果检查 order 的类型,显示为 NoneType 类型。因此,你需要确保在 iadd() 的实现中返回期望得到的结果而不是其他什么东东。

    iadd() 类似, isub(), imul(), idiv() 等特定方法相应地定义了 -=, *=, /= 等运算符的行为。

    注意:当 iadd() 或者同系列的方法没有在你的类中定义,而你又在你的对象上使用这些运算符时。Python 会用 add() 系列方法来替代并返回结果。通常来讲,如果 add() 系列方法能够返回预期正确的结果,不使用 iadd() 系列的方法是一种安全的方式。

    Python 的文档提供了这些方法的详细说明。此外,可以看看当使用不可变类型涉及到的 +=及其他运算符需要注意到的附加说明的代码实例。

    使用 [] 运算符来索引和分片你的对象

    [] 运算符被称作索引运算符,在 Python 各上下文中都有用到,比如获取序列某个索引的值,获取字典某个键对应的值,或者对序列的切片操作。你可以通过 getitem() 特定方法来控制该运算符的行为。

    我们设置一下 Order 类的定义,让我们可以直接获取购物车对象中的项:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __getitem__(self, key):
    ...         return self.cart[key]
    ...
    >>> order = Order(['banana', 'apple'], 'Real Python')
    >>> order[0]
    'banana'
    >>> order[-1]
    'apple'
    

    你可能会注意到上面的例子中, getitem() 方法的参数名并不是 index 而是 key。这是因为,参数主要接收三种类型的值:整数值,通常是一个索引或字典的键值;字符串,字典的键值;切片对象,序列对象的切片。当然,也可能会有其他的值类型,但这三种是最常见的形式。
    因为我们的内部数据结构是一个列表,我们可以使用 [] 运算符来对列表进行切片,这时 key 参数会接收一个切片对象。这就是在类中定义 getitem() 方法的最大优势。只要你使用的数据结构支持切片操作(列表、元组、字符串等等),你就可以定义你的对象直接对数据进行切片:

    >>> order[1:]
    ['apple']
    >>> order[::-1]
    ['apple', 'banana']
    

    注意:有一个类似的 setitem() 特定方法可定义类似 obj[x] = y 这种行为。此方法除自身外还需要两个参数,一般称为 key 和 value,用来更改指定 key 索引的值。

    逆运算符:让你的类在数学计算上正确

    在你定义了 add(), sub(), mul(),以及类似的方法后,类实例作为左侧操作数时可以正确运行,但如果作为右侧操作数则不会正常工作:

    >>> class Mock:
    ...     def __init__(self, num):
    ...         self.num = num
    ...     def __add__(self, other):
    ...         return Mock(self.num + other)
    ...
    >>> mock = Mock(5)
    >>> mock = mock + 6
    >>> mock.num
    11
    
    >>> mock = 6 + Mock(5)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unsupported operand type(s) for +: 'int' and 'Mock'
    

    如果你的类表示的是一个数学实体,比如向量、坐标或复数,运算符应该在这两种方式下都能正确运算,因为它是有效的数学运算规则。此外,如果某个运算符仅仅在操作数为左侧时才工作,这在数学上违背了交换律规则。因此,为了保证在数学上的正确,Python 为你提供了反向计算的 radd(), rsub(), rmul()等特定方法。

    这些方法处理类似 x + obj, x - obj, 以及 x * obj 形式的运算,其中 x 不是一个类实例对象。和 add() 及其他方法一样,这些方法也应该返回一个新的实例对象,而不是修改自身。
    我们在 Order 类中定义 radd() 方法,这样就可以将某些项操作数放在购物车对象前面进行添加。这还可以用在购物车内订单是按照优先次序排列的情况。:

    >>> class Order:
    ...     def __init__(self, cart, customer):
    ...         self.cart = list(cart)
    ...         self.customer = customer
    ...
    ...     def __add__(self, other):
    ...         new_cart = self.cart.copy()
    ...         new_cart.append(other)
    ...         return Order(new_cart, self.customer)
    ...
    ...     def __radd__(self, other):
    ...         new_cart = self.cart.copy()
    ...         new_cart.insert(0, other)
    ...         return Order(new_cart, self.customer)
    ...
    >>> order = Order(['banana', 'apple'], 'Real Python')
    
    >>> order = order + 'orange'
    >>> order.cart
    ['banana', 'apple', 'orange']
    
    >>> order = 'mango' + order
    >>> order.cart
    ['mango', 'banana', 'apple', 'orange']
    

    一个完整的例子

    想要掌握以上所有的关键点,最好自己实现一个包含以上所有操作的自定义类。我们自己来造一个轮子,实现一个复数的自定义类 CustomComplex。这个类的实例将支持各种内置函数和运算符,行为表现上非常类似于 Python 自带的复数类:

    from math import hypot, atan, sin, cos
    
    class CustomComplex:
        def __init__(self, real, imag):
            self.real = real
            self.imag = imag
    

    构造函数只支持一种调用方式,即 CustomComplex(a, b)。它通过位置参数来表示复数的实部和虚部。我们在这个类中定义两个方法 conjugate() 和 argz()。它们分别提供复数共轭和复数的辐角:

    def conjugate(self):
        return self.__class__(self.real, -self.imag)
    
    def argz(self):
        return atan(self.imag / self.real)
    

    注意: class 并不是特定方法,只是默认的一个类属性通常指向类本身。这里我们跟调用构造函数一样来对它进行调用,换句话来说其实调用的就是 CustomComplex(real, imag)。这样调用是为了防止今后更改类名时要再次重构代码。
    下一步,我们配置 abs() 返回复数的模:

    def __abs__(self):
        return hypot(self.real, self.imag)
    

    我们遵循官方建议的 repr() 和 str() 两者差异,用第一个来实现可解析的字符串输出,用第二个来实现“更美观”的输出。 repr() 方法简单地返回 CustomComplex(a, b) 字符串,这样我们在调用 eval() 重建对象时很方便。 str() 方法用来返回带括号的复数输出形式,比例 (a+bj):

    def __repr__(self):
        return f"{self.__class__.__name__}({self.real}, {self.imag})"
    
    def __str__(self):
        return f"({self.real}{self.imag:+}j)"
    

    数学上讲,我们可以进行两个复数相加或者将一个实数和复数相加。我们定义 + 运算符来实现这个功能。方法将会检测运算符右侧的类型,如果是一个整数或者浮点数,它将只增加实部(因为任意实数都可以看做是 a+0j),当类型是复数时,它会同时更改实部和虚部:

    def __add__(self, other):
        if isinstance(other, float) or isinstance(other, int):
            real_part = self.real + other
            imag_part = self.imag
    
        if isinstance(other, CustomComplex):
            real_part = self.real + other.real
            imag_part = self.imag + other.imag
    
        return self.__class__(real_part, imag_part)
    

    同样,我们定义 -* 运算符的行为:

    def __sub__(self, other):
        if isinstance(other, float) or isinstance(other, int):
            real_part = self.real - other
            imag_part = self.imag
    
        if isinstance(other, CustomComplex):
            real_part = self.real - other.real
            imag_part = self.imag - other.imag
    
        return self.__class__(real_part, imag_part)
    
    def __mul__(self, other):
        if isinstance(other, int) or isinstance(other, float):
            real_part = self.real * other
            imag_part = self.imag * other
    
        if isinstance(other, CustomComplex):
            real_part = (self.real * other.real) - (self.imag * other.imag)
            imag_part = (self.real * other.imag) + (self.imag * other.real)
    
        return self.__class__(real_part, imag_part)
    

    因为加法和乘法可以交换操作数,我们可以在反向运算符 radd() 和 rmul() 方法中这样调用 add() 和 mul() 。此外,减法运算的操作数是不可以交换的,所以需要 rsub() 方法的行为:

    def __radd__(self, other):
        return self.__add__(other)
    
    def __rmul__(self, other):
        return self.__mul__(other)
    
    def __rsub__(self, other):
        # x - y != y - x
        if isinstance(other, float) or isinstance(other, int):
            real_part = other - self.real
            imag_part = -self.imag
    
        return self.__class__(real_part, imag_part)
    

    注意:你也许发现我们并没有增添一个构造函数来处理 CustomComplex 实例。因为这种情形下,两个操作数都是类的实例, rsub() 方法并不负责处理实际的运算,仅仅是调用 sub() 方法来处理。这是一个微妙但是很重要的细节。
    现在我们来看看另外两个运算符:== 和 != 。这两个分别对应的特定方法是 eq() 和 ne()。如果两个复数的实部和虚部都相同则两者是相等的。只要两个部分任意一个不相等两者就不相等:

    def __eq__(self, other):
        # Note: generally, floats should not be compared directly
        # due to floating-point precision
        return (self.real == other.real) and (self.imag == other.imag)
    
    def __ne__(self, other):
        return (self.real != other.real) or (self.imag != other.imag)
    

    注意:浮点指南这篇文章讨论了浮点数比较和浮点精度的问题,它涉及到一些浮点数直接比较的一些注意事项,这与我们在这里要处理的情况有点类似。
    同样,我们也可以通过简单的公式来提供复数的幂运算。我们通过定义 pow() 特定方法来设置内置函数 pow() 和 ** 运算符的行为:

    def __pow__(self, other):
        r_raised = abs(self) ** other
        argz_multiplied = self.argz() * other
    
        real_part = round(r_raised * cos(argz_multiplied))
        imag_part = round(r_raised * sin(argz_multiplied))
    
        return self.__class__(real_part, imag_part)
    

    注意:认真看看方法的定义。我们调用 abs() 来获取复数的模。所以,我们一旦为特定功能函数或运算符定义好了特定方法,它就可以被用于此类的其他方法中。

    我们创建这个类的两个实例,一个拥有正的虚部,一个拥有负的虚部:

    >>> a = CustomComplex(1, 2)
    >>> b = CustomComplex(3, -4)
    

    字符串表示:

    >>> a
    CustomComplex(1, 2)
    >>> b
    CustomComplex(3, -4)
    >>> print(a)
    (1+2j)
    >>> print(b)
    (3-4j)
    

    使用 eval() 和 repr()重建对象

    >>> b_copy = eval(repr(b))
    >>> type(b_copy), b_copy.real, b_copy.imag
    (__main__.CustomComplex, 3, -4)
    

    加减乘法:

    >>> a + b
    CustomComplex(4, -2)
    >>> a - b
    CustomComplex(-2, 6)
    >>> a + 5
    CustomComplex(6, 2)
    >>> 3 - a
    CustomComplex(2, -2)
    >>> a * 6
    CustomComplex(6, 12)
    >>> a * (-6)
    CustomComplex(-6, -12)
    

    相等和不等检测:

    >>> a == CustomComplex(1, 2)
    True
    >>> a ==  b
    False
    >>> a != b
    True
    >>> a != CustomComplex(1, 2)
    False
    
    最后,将复数加到某个幂上:
    
    ```python
    >>> a ** 2
    CustomComplex(-3, 4)
    >>> b ** 5
    CustomComplex(-237, 3116)
    

    正如你所见到的,我们自定义类的对象外观及行为上类似于内置的对象而且很 Pythonic。

    回顾总结

    本教程中,你学习了 Python 数据模型,以及如何通过数据模型来构建 Pythonic 的类。学习了改变 len(), abs(), str(), bool() 等内置函数的行为,以及改变 +, -, *, **, 等内置运算符的行为。
    如果想要进一步地了解数据模型、函数和运算符重载,请参考以下资源:

    • Python 文档,数据模型的第 3.3 节,特定方法名
    • 流畅的 Python(Fluent Python by Luciano Ramalho)
    • Python 技巧(Python Tricks)

    本示例的完整代码:

    from math import hypot, atan, sin, cos
    
    class CustomComplex():
        """
        A class to represent a complex number, a+bj.
        Attributes:
            real - int, representing the real part
            imag - int, representing the imaginary part
    
        Implements the following:
    
        * Addition with a complex number or a real number using `+`
        * Multiplication with a complex number or a real number using `*`
        * Subtraction of a complex number or a real number using `-`
        * Calculation of absolute value using `abs`
        * Raise complex number to a power using `**`
        * Nice string representation using `__repr__`
        * Nice user-end viewing using `__str__`
    
        Notes:
            * The constructor has been intentionally kept simple
            * It is configured to support one kind of call:
                CustomComplex(a, b)
            * Error handling was avoided to keep things simple
        """
    
        def __init__(self, real, imag):
            """
            Initializes a complex number, setting real and imag part
            Arguments:
                real: Number, real part of the complex number
                imag: Number, imaginary part of the complex number
            """
            self.real = real
            self.imag = imag
    
        def conjugate(self):
            """
            Returns the complex conjugate of a complex number
            Return:
                CustomComplex instance
            """
            return CustomComplex(self.real, -self.imag)
    
        def argz(self):
            """
            Returns the argument of a complex number
            The argument is given by:
                atan(imag_part/real_part)
            Return:
                float
            """
            return atan(self.imag / self.real)
    
        def __abs__(self):
            """
            Returns the modulus of a complex number
            Return:
                float
            """
            return hypot(self.real, self.imag)
    
        def __repr__(self):
            """
            Returns str representation of an instance of the 
            class. Can be used with eval() to get another 
            instance of the class
            Return:
                str
            """
            return f"CustomComplex({self.real}, {self.imag})"
    
    
        def __str__(self):
            """
            Returns user-friendly str representation of an instance 
            of the class
            Return:
                str
            """
            return f"({self.real}{self.imag:+}j)"
    
        def __add__(self, other):
            """
            Returns the addition of a complex number with
            int, float or another complex number
            Return:
                CustomComplex instance
            """
            if isinstance(other, float) or isinstance(other, int):
                real_part = self.real + other
                imag_part = self.imag
    
            if isinstance(other, CustomComplex):
                real_part = self.real + other.real
                imag_part = self.imag + other.imag
    
            return CustomComplex(real_part, imag_part)
    
        def __sub__(self, other):
            """
            Returns the subtration from a complex number of
            int, float or another complex number
            Return:
                CustomComplex instance
            """
            if isinstance(other, float) or isinstance(other, int):
                real_part = self.real - other
                imag_part = self.imag
    
            if isinstance(other, CustomComplex):
                real_part = self.real - other.real
                imag_part = self.imag - other.imag
    
            return CustomComplex(real_part, imag_part)
    
        def __mul__(self, other):
            """
            Returns the multiplication of a complex number with
            int, float or another complex number
            Return:
                CustomComplex instance
            """
            if isinstance(other, int) or isinstance(other, float):
                real_part = self.real * other
                imag_part = self.imag * other
    
            if isinstance(other, CustomComplex):
                real_part = (self.real * other.real) - (self.imag * other.imag)
                imag_part = (self.real * other.imag) + (self.imag * other.real)
    
            return CustomComplex(real_part, imag_part)
    
        def __radd__(self, other):
            """
            Same as __add__; allows 1 + CustomComplex('x+yj')
            x + y == y + x
            """
            pass
    
        def __rmul__(self, other):
            """
            Same as __mul__; allows 2 * CustomComplex('x+yj')
            x * y == y * x
            """
            pass
    
        def __rsub__(self, other):
            """
            Returns the subtraction of a complex number from
            int or float
            x - y != y - x
            Subtration of another complex number is not handled by __rsub__
            Instead, __sub__ handles it since both sides are instances of
            this class
            Return:
                CustomComplex instance
            """
            if isinstance(other, float) or isinstance(other, int):
                real_part = other - self.real
                imag_part = -self.imag
    
            return CustomComplex(real_part, imag_part)
    
        def __eq__(self, other):
            """
            Checks equality of two complex numbers
            Two complex numbers are equal when:
                * Their real parts are equal AND
                * Their imaginary parts are equal
            Return:
                bool
            """
            # note: comparing floats directly is not a good idea in general
            # due to floating-point precision
            return (self.real == other.real) and (self.imag == other.imag)
    
        def __ne__(self, other):
            """
            Checks inequality of two complex numbers
            Two complex numbers are unequal when:
                * Their real parts are unequal OR
                * Their imaginary parts are unequal
            Return:
                bool
            """
            return (self.real != other.real) or (self.imag != other.imag)
    
        def __pow__(self, other):
            """
            Raises a complex number to a power
            Formula:
                z**n = (r**n)*[cos(n*agrz) + sin(n*argz)j], where
                z = complex number
                n = power
                r = absolute value of z
                argz = argument of z
            Return:
                CustomComplex instance
            """
            r_raised = abs(self) ** other
            argz_multiplied = self.argz() * other
    
            real_part = round(r_raised * cos(argz_multiplied))
            imag_part = round(r_raised * sin(argz_multiplied))
    
            return CustomComplex(real_part, imag_part)
    

    本作品采用:知识共享 署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

    相关文章

      网友评论

        本文标题:Python 自定义类中的函数和运算符重载

        本文链接:https://www.haomeiwen.com/subject/bwgtxqtx.html