系统学习Python——类(class)代码的编写基础与实例:类可以截获Python运算符

分类目录:《系统学习Python》总目录


现在,让我们来看类和模块的第三个也是最后一个主要差别:运算符重载。简而言之,运算符重载就是让用类编写的对象,可截获并响应用在内置类型上的运算:加法、切片、打印和点号运算等。这其实只是一种自动分发机制:表达式和其他内置运算被路由到了类内部的实现。在这点上类和模块也基本不同:模块可以实现函数调用,但却不能实现表达式的行为。

虽然我们可以把所有类的行为实现为方法函数,运算符重载则让对象和Python的对象模型更紧密地结合起来。此外,因为运算符重载能让我们自己的对象拥有内置对象那样的行为,所以这既可以让对象接口更为一致并且更易于学习,又可以让类对象被预期内置类型接口的代码来处理。以下是重载运算符主要概念的概要:

  • 以双下划线命名的方法__X__是特殊钩子:在Python类中我们实现运算符重载是通过提供特殊命名的方法来拦截运算。Python语言在每种运算和特殊命名的方法之间,定义了固定不变的映射关系。
  • 当实例出现在内置运算中时,这类方法会自动被调用。例如,如果实例对象继承了一个__add__方法,那么当对象出现在+表达式内时,该方法就会被调用。而该方法的返回值将作为相应表达式的结果。
  • 类可以重载绝大多数内置类型运算。Python中有几十种特殊运算符重载的方法的名称,几乎可截获并实现内置类型的所有运算。它不仅包括了表达式,同时还包括像打印和对象创建这样的基础运算。
  • 默认的运算符重载方法既不存在,也不需要。如果类没有定义或继承运算符重载法,那么类的实例将不能支持相应的运算。例如,如果没有__add__+表达式就会引发异常。
  • 新式类有一些默认的运算符重载方法,但是不属于常见运算。在Python所谓新式类中,一个名为object的根类确实提供了某些默认的__X__方法。但是提供的不多,同时也不属于大多数常见的运算
  • 运算符将类与Python的对象模型结合到一起。通过重载类型运算,我们可以让采用类实现的用户定义对象获得与内置对象一样的行为,因此这保证了与预期接口的一致性和兼容性。

运算符重载是可选的功能。它主要被Python工具开发人员使用,而不是那些应用程序开发人员。此外,坦率地说,不应该因为运算符重载看起来很聪明或是很“酷”就随意去使用。除非类需要模仿内置类型接口,否则你应该使用更简单的命名方法。例如,员工数据库应用程序为什么要支持像*+这类表达式呢?通常来说,有着像giveRaisepromote这样的名称的方法是更有意义的。

因此,我们不会在本文中深入讨论Python中每一个可用的运算符重载方法。不过,有一个运算符重载方法你可能会在每个实际的Python类中都遇到:__init__方法,也称为构造函数方法,它是用于初始化对象的状态的。你应该特别注意这个方法,因为__init__self参数是阅读和理解Python的OOP程序代码的关键之一。

下面是另一个例子,我们要定义《类(class)代码的编写基础与实例:类通过继承进行定制》的SecondClass的子类,实现Python会自动调用的三个特殊名称属性:

  • __init__会在创建新的实例时被调用:self是新的ThirdClass对象
  • __add__会在ThirdClass实例出现在+表达式中时被调用。
  • __str__会在打印一个对象的时候。从技术上讲,当通过__str__内置函数或者其Python内部的等价形式来将对象转换为打印字符串的时候被调用。

我们先定义ThirdClass类:

class ThirdClass(SecondClass):
    def __init__(self, value):
        self.data = value
    
    def __add__(self, other):
        return ThirdClass(self.data + other)
    
    def __str__(self):
        return 'ThirdClass: %s' % self.data
    
    def mul(self, other):
        self.data *= other

然后初始化一个类实例对象并执行一些操作:

a = ThirdClass('hy592070616')
a.display()
print(a)

输出:

Blog="hy592070616"
ThirdClass: hy592070616

然后通过重载的内置方法__add__实现+的运算:

b = a + ' MachineLearning'
b.display()
print(b)

输出:

Blog="hy592070616 MachineLearning"
ThirdClass: hy592070616 MachineLearning

测试ThirdClass类的其它方法:

a.mul(3)
print(a)

输出:

ThirdClass: hy592070616hy592070616hy592070616

ThirdClass是一个SecondClass对象,所以其实例会继承《类(class)代码的编写基础与实例:类通过继承进行定制》的SecondClassdisplay方法。但是,ThirdClass生成的调用现在会传人一个参数(例如:abc),这是传给__init__构造函数内的参数value的,并在构造函数中被赋值给self.data。最终的效果是ThirdClass会在创建时自动设置data属性,而不再是必须在构建之后通过setdata调用。

此外,ThirdClass对象现在可以出现在+表达式和print调用中。对于+,Python把左侧的实例对象传给__add__中的self参数,而把右侧的对象传给other,如下图所示。而__add__返回的内容则成为+表达式的结果。对于print,Python把要打印的对象传给__str__中的self,该方法返回的字符串看作对象的打印字符串。我们可以用一个常规的print来显示该类的对象,而不是调用特殊的display方法。
__add__函数
__init____add____str__这样的特殊命名方法会由子类和实例继承,就像一个class语句中被赋值的其他名称。如果这些名称没有被编写在类中,那么Python就会在该类的所有父类中寻找这类变量名。运算符重载方法的名称并不是内置变量或保留字,它们只是当对象出现在不同的上下文时Python会自动去搜索的属性。Python通常会自动进行调用,但偶尔也能被你所编写的程序代码调用。

是否返回结果

一些像__str__的运算符重载方法要求结果,但另一些则更加灵活。例如,注意,__add__方法是如何通过结果值调用ThirdClass,从而创建并返回一个该类新的实例对象的。也就是说,这反过来会触发__add__将结果初始化。这是一个常见的约定,也解释了为什么程序清单中的b有一个display方法;因为在该类的对象上调用+会返回一个新的该类对象,所以b也是一个ThirdClass对象。这本质上传播了该类型。

相比之下,mul会在原位置修改当前的实例对象(通过重新赋值self属性)。我们可以通过重载*表达式来实现mul,但这将与内置的数字与字符串的行为极其不同,因为这里ThirdClass对象的*运算符总是创建新对象。实践证明,重载的运算符应该以与内置的运算符实现同样的方式工作。因为运算符重载其实只是一种表达式对方法的分发机制,所以你可以在自己的类对象中以任何喜欢的方式解释运算符。

为什么要使用运算符重载

作为一名类的设计者,你可以选择是否要使用运算符重载。你的决定取决于有多想让对象的用法和外观看起来更像内置类型。就像前面提到的那样,如果省略运算符重载方法而且不从父类中继承该方法,那么实例就不支持相应的运算:如果试着使用这个实例进行运算,就会引发异常(或者在一些类似打印的情形下,使用标准的默认运算符重载方法)。坦率地讲,只有在实现具有数学本质的对象时,才会用到许多运算符重载方法。例如,向量或矩阵类可以重载加法运算符,但员工类可能就不用。就较简单的类而言,可能根本不会用到重载,因此应该利用显式的方法调用来实现对象的行为。

另外,如果需要把用户定义的对象传入预期接受并使用内置类型(例如列表或字典)可用的运算符的函数,那么你可能就会决定使用运算符重载。在类中实现同一组运算符,可以保证对象能支持相同的预期的对象接口,因此会与这个函数兼容。

这里我们会经常使用的一个重载方法是__init__构造函数,它用来初始化新建的实例对象,并出现在几乎每一个实际的类中。因为__init__可以让类在新产生的实例中立即添加属性,所以对每种你将写的类而言构造函数都是有用的。事实上,虽然Python不会对实例的属性进行声明,但你通常也可以通过阅读类的__init__方法的代码来看看实例有哪些属性。当然,尝试一下好玩的语言工具是无可厚非的,但它们并不总是能转化为产品代码。假以时日和经验,我们将会自然且熟练地使用这些编程模式和准则。

猜你喜欢

转载自blog.csdn.net/hy592070616/article/details/126274647