如果你对 Python 中的str对象使用过 + 或 * 运算符,你一定注意到了它的操作与 int 或 float 类型的区别:
你可能想知道同一内置运算符或函数如何对不同类对象进行不同操作的。这分别称为运算符重载和函数重载。本文将帮助你了解此机制,以便你可以在自己的 Python 类中执行相同的过程,使对象更 Pythonic。
你将了解以下内容:
处理 Python 中的运算符和内置函数的 API
len() 和其他内置函数背后的 "秘密"
如何使你的类能够使用运算符
如何使你的类与 Python 的内置函数兼容
除此之外你还能看到一个示例类,其中的对象与许多运算符和函数兼容。我们开始吧!
假设有一个表示在线订单的类,具有购物车 (列表) 和顾客 (代表顾客的str或其他类的实例)两种数据。
在这种情况下,要获得购物车列表的长度是很自然的。新接触 Python 的人可能会选择在他们的类中实现一个叫get_cart_len()的方法来执行此项。但是,你也可以配置内置函数 len(),以便在给定对象时返回购物车列表的长度。
在另一种情况下, 我们可能需要添加一些东西到购物车。再次,新接触 Python 的人会想到实现一个 append_to_cart() 方法,以添加东西到购物车列表。但是你也可以配置运算符 + ,用它来将新内容添加到购物车中。
Python 使用特殊的方法来做这些。这些特殊方法具有命名约定,其中名称以两个下划线开头, 后跟一个标识符, 并以另一对下划线结尾。
本质上, 每个内置函数或运算符都有一个与之对应的特殊方法。例如,对应于 len() 有 __len__() ,对应于运算符 + 有__add__()。
默认情况下, 大多数内置函数和运算符都不能与自定义类的对象一起使用。必须在类定义中添加相应的特殊方法,才能使对象与内置和运算符兼容。
执行此操作时,与其关联的函数或运算符的行为将根据方法中定义的方式进行更改。
这正是数据模型(Python 文档的3节) 帮助你完成的内容。它列出了所有可用的特殊方法,并为你提供了重载内置函数和运算符的方法, 以便你可以在自己的对象上使用它们。
让我们看看这意味着什么。
有趣的事实:由于这些方法使用的命名约定, 它们也被称为dunder 方法,这是对double underscore的缩写。有时它们也被称为特殊方法或魔术方法。不过,我们更喜欢叫它dunder 方法!
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 是执行调用你的方法。让我们深入了解一下:
正如你所看到的,当你使用该函数或其相应的特殊方法时, 将得到相同的结果。事实上,当你使用 dir() 获取 str 对象的属性和方法列表时,除了str对象上可用的常用方法外,还将在列表中看到这些特殊方法:
如果在类中未通过特殊方法重新定义内置函数或运算符,则会返回TypeError。
那么,如何在你的类中使用特殊的方法?
数据模型中定义的许多特殊方法如 len, abs, hash, divmod等等可用于更改函数的行为。为此,你只需在类中定义相应的特殊方法。让我们看几个例子:
若要更改 len() 的操作,需要在类中定义特殊方法 __len__() 。当你将自定义类的对象传递给 len() 时,自定义定义将用于获取结果。让我们来实现我们一开始讨论的类 len():
正如你所看到的,现在可以使用 len() 来直接获取购物车的长度。此外,说 "订单的长度"比调用类似order.get_cart_len()的方法更直观。你的调用既 Pythonic 又直观。如果没有定义__len__()方法但仍对你的对象调用len(),则会遇到TypeError:
但是,当重载len()时,你应该记住 Python 需要函数返回一个整数。如果你的方法是返回一个整数以外的任何东西,你会得到TypeError。这很可能是为了使它与 len() 通常用于获取序列长度这一事实保持一致,而这只能是一个整数:
通过定义类中的特殊方法 __abs__() 可以为类的实例重写内置abs()的操作。abs() 的返回值没有限制,而当你的类定义中缺少特殊方法时, 就会返回TypeError。
在表示二维空间中向量的类中,可用 abs() 获取向量的长度。让我们来看看它的实际过程:
说 "向量绝对值"比调用类似vector.get_mag()的方法更直观。
内置的str()用于将类的实例强制转换为str对象,或者更准确地说是获取对象的用户友好型的字符串表示形式,而这可以被普通用户而不是程序员接受。通过在类中定义__str__()方法,在传递str()时可以定义对象应显示的字符串格式。此外,__str__()是 Python 在你对对象调用print()时使用的方法。
让我们在Vector类中实现此目的,使Vector对象的格式化为xi+yj。对y为负值的部分将使用 "迷你语言" 格式处理:
__str__()返回一个str对象是必要的,如果返回类型是非字符串, 则得到TypeError。
内置的repr()用于获取对象的解析字符串表示形式。如果一个对象是解析的,这意味着当repr与函数eval()一起使用时 Python 能够从表示形式中重新创建对象。若要定义repr()的行为,可以使用特殊方法__repr__()。
这也是 Python 用于在 REPL 会话中显示对象的方法。如果未定义__repr__()方法,你将得到类似于<__main__.Vector object at 0x...>一样尝试查看 REPL 会话中的对象内容。让我们在Vector类里查看它的作用:
注:在未定义__str__()方法的情况下,Python 使用__repr__()方法来打印对象,并在调用str()时表示该对象。如果两种方法都缺失, 则默认为<__main__.Vector ...>。而__repr__()是用于在交互式会话中显示对象的唯一方法。类中缺失它则会默认为<__main__.Vector ...>。
此外,虽然__str__()和__repr__()有这种区分,许多流行的库是忽略这种区别的,并混杂使用这两种方法。
下面推荐我们自己的一篇介绍__repr__()和__str__()的文章:https://dbader.org/blog/python-repr-vs-str。
内置的bool()可用于获取对象的布尔值。若要重新定义其行为,可以使用特殊方法__bool__() (在 Python 2.x 中是__nonzero__()) 。
此处定义的行为将在所有需要获取布尔值的情境 (如if语句) 中确定实例是否为真。
例如,对于上面定义的Order类,如果购物车列表的长度是非零,则可以认为实例是真。这可用于检查是否应处理订单:
注:当在类中未实现特殊方法__bool__()时,__len__()所返回的值将用作布尔值,其中非零值为真,零值为假。如果两种方法都未实现,则将该类的所有实例视为真。
有许多更特殊的方法重载内置函数。你可以在文档中找到它们。讨论了其中的一些,接下来让我们看看运算符。
英文原文:https://realpython.com/operator-function-overloading/
译者:β