Python 重载与覆盖


2022年11月24日, Learn eTutorial
2954

在本教程中,您将掌握面向对象编程的另一个重要概念,即多态,以及在 Python 中实现多态的不同方法,例如

  • 重载,
  • 重写,
  • 鸭子类型

等概念,并进行详细讲解。

Python 中的多态是什么?

多态是面向对象编程的基本构建块之一。为了更清晰地理解它,让我们将“多态”一词分解为“poly”和“morphism”。通俗地说,“poly”表示“多”,“morph”表示“形态”。所以多态就是“多种形态”。

Polymorphism in python

Python 中的多态

在编程中,多态可以定义为单个实体具有多种形态的过程。Python 作为一个 exclusively works with objects 的语言,在对象上体现了多态性,这意味着对象可以呈现多种形态。

在现实生活中,我们人类就是多态的,因为我们在不同的情境下表现不同。例如,我们在办公室和在家的行为方式就不同。

Python 中的鸭子类型是什么?

鸭子类型是实现多态的一种方式,它与动态类型密切相关。鸭子类型得名于一句名言:“如果一个东西像鸭子一样走路,像鸭子一样叫,像鸭子一样游泳,那么它就应该是一只鸭子”。这意味着,如果某个东西的行为举止像鸭子,那么它就是一只鸭子。例如,如果一个人像鸭子一样走路,像鸭子一样游泳,那么这个人可能就是一只鸭子。

Python 和 Javascript 等支持动态类型的编程语言广泛使用了鸭子类型概念。这些语言独特且常见的特点是它们不显式声明变量类型。如果我们用另一种数据类型重新赋值同一个变量,编译器不会报错。请观察以下示例:

x = 5
print(type(x))
x = 'Learn eTutorial'
print(type(x))

输出


<class 'int'> 
<class 'str'>

在这里,我们最初将变量 x 赋值为整数值 5,使其成为 int 类型。稍后,我们用字符串值‘Learn eTutorials’重新赋值同一个变量,使其成为 str 类型。这表明动态类型适用于 int、str、list 等内置类。

同样,我们可以通过鸭子类型在自定义类上实现动态类型。在鸭子类型中,重点在于方法和属性的存在,我们不检查类型和类,因为它们不太重要。

class Duck:
    def walk_swim(self):
        print("I'm a duck, and I can walk and swim.")

class Robot:
     def walk_swim(self):
         print("I'm a Robot, and I can walk and swim.")

class Fish:
     def swim(self):
         print("I'm a fish, and I can swim, but not walk.")
 
for bird in Duck(),Robert(),Fish():
    bird.walk_swim() 

输出

I'm a duck, and I can walk and swim.
I'm a Robot, and I can walk and swim.
Traceback (most recent call last):
  File "poly_ex.py", line 118, in 
    bird.walk_swim()
AttributeError: 'Fish' object has no attribute 'walk_swim'

在上面的代码片段中,bird 是每个类的对象,用于测试每个类中 walk_swim() 方法的存在性。Duck 类通过了测试,因为它确实可以走路和游泳。Robot 类也是如此,因为它也实现了 walk_swim() 方法。但是 Fish 类最终出错,因为它没有实现 walk_swim() 方法,未能通过鸭子测试评估。

鸭子类型的实际应用包括迭代器、可调用对象以及 len() 方法的排序。关键要点是,在使用鸭子类型时,传递什么类的对象并不重要,重要的是对象是否具有相关的方法。

Python 中的运算符重载是什么?

在 Python 中实现多态的另一种方式是通过运算符重载。回想一下什么是运算符。任何执行某种计算的独特符号都称为运算符。所以运算符重载是使用相同的运算符,例如 +,根据使用的操作数,在多种形态中进行操作的过程。查看下面的代码片段,展示了运算符‘+’行为的变化:

a = 1
b = 2
print(a+b)

c ='Python'
print(c*3)

d = 'Programming'
e = 'Language'
print(d+e) 

输出

3
PythonPythonPython
ProgrammingLanguage

在这里,加号运算符对两个整数执行加法,同时对两个字符串执行连接。

您可能想知道这是如何可能的,以及此运算符背后实际发生了什么。让我们以两个整数相加为例,对其进行内部分解,以了解其背后的过程。

始终记住,Python 中发生的一切都完全基于对象。这里 a+b 是一个带有两个操作数的表达式,操作数 a 和 b 的类型是 'int',这意味着 a 和 b 是int 类的两个属性。显然,类必须有一些方法来计算其操作数。所以当你使用 a+b 时,Python 会在内部调用 魔术方法 - int.__add__(a,b)方法。

a = 1
b = 2
print(int.__add__(a,b))

c ='Python'
print(str.__mul__('Python',3))

d = 'Programming'
e = 'Language'
print(str.__add__('Programming','Language')) 

输出

3
PythonPythonPython
ProgrammingLanguage

所以当你使用一个

  • + 运算符时,它会调用一个 obj.__add__() 方法。
  • - 运算符调用 obj.__sub__() 方法。
  • * 运算符调用 obj.__mul__() 方法。

在 Python 中,运算符重载适用于 int、str、list 等内置类。但是,我们也可以将运算符的可操作性扩展到用户定义的类。这可以通过运算符重载来实现。

class Quantity:
    def __init__(self,w1,w2):
        self.w1=w1
        self.w2=w2

    def __str__(self):
        return "{} ,{}".format(self.w1,self.w2)

q1=Quantity(5,6)
q2=Quantity(7,8)

print(q1)
print(q2)

print(q1+q2)


输出

(5 ,6)
(7 ,8) 
Traceback (most recent call last):
  File "poly_ex.py", line 51, in 
    print(q1+q2)
TypeError: unsupported operand type(s) for +: 'Quantity' and 'Quantity'

让我们分解示例以清楚地理解它。

  •  我们定义了一个名为 Quantity 的类,它有两个属性 w1w2
  • __str__() 方法与 print 函数的作用相同。这里,此方法返回指定的数量。
  • q1q2 是我们创建的两个实例,分别具有两个不同的数量(5,6)和(7,8)。
  • 接下来,我们尝试单独和组合地打印实例中的属性。
  • 从示例中可以清楚地看到,print(q1) 和 print(q2) 可以顺利运行,但 print(q1+q2) 语句会引发一个名为 Type Error 的错误。这是因为我们没有定义要对操作数执行的操作,它只适用于内置类。

那么,我们如何以用户定义的方式实现运算符的可操作性呢?我们只需要在类中定义一个额外的方法,使 + 运算符可以对对象进行操作。为此,我们在类中实现了魔术方法 __add__()

class Quantity:
    def __init__(self,w1,w2):
        self.w1=w1
        self.w2=w2

    def __str__(self):
        return "{} ,{}".format(self.w1,self.w2)

    def __add__(self,other):
        w1=self.w1+other.w1
        w2=self.w2+other.w2
        return (w1,w2)

q1=Quantity(5,6)
q2=Quantity(7,8)

print(q1)
print(q2)

print(q1+q2) 

输出

(5 ,6)
(7 ,8)
(12, 14)

当你调用 print(q1+q2) 时,Python 会调用 add 方法,形式为 Quantity.__add__(q1,q2),这等同于 q1.__add__(q2)。

Python中的魔术方法

魔术方法双下划线方法是 Python 中前后都有双下划线的方法。Dunder 是 Double Under 的缩写。下表列出了一些常用的 Python 魔术方法。

运算符 表达式 魔术方法
加法 q1+ q2 ob.__add__(q1,q2)
减法 q1 – q2 ob.__sub__(q1,q2)
乘法 q1 * q2 ob.__mul__(q1,q2)
除法 q1 / q2 ob.__truediv__(q1,q2)
q1 ** q2 ob.__pow__(q1,q2)
整除 q1 // q2 ob.__floordiv__(q1,q2)
模运算符 q1 % q2 ob.__mod__(q1,q2)
按位左移 q1 << q2 ob.__lshift__(q1,q2)
按位右移 q1 >> q2 ob.__rshift__(q1,q2)
按位非 ~q1 ob.__invert__(q1)
按位与 q1 & q2 ob.__and__(q1,q2)
按位或 q1 | q2 ob.__or__(q1,q2)
按位异或 q1 ^ q2 ob.__xor__(q1,q2)
小于 q1 < q2 ob.__lt__(q1,q2)
小于等于 q1 <= q2 ob.__le__(q1,q2)
大于 q1 > q2 ob.__gt__(q1,q2)
大于等于 q1 >= q2 ob.__ge__(q1,q2)
等于 q1 == q2 ob.__eq__(q1,q2)
不等于 q1 !=  q2 ob.__ne__(q1,q2)

Python 方法/函数重载

方法重载,在面向对象编程中,是指方法根据传递给它的参数以不同方式表现的能力。方法重载支持编译时多态

G说得更清楚一点,如果你有一个类,其中有两个同名但参数数量不同的方法,那么这个方法就被称为重载方法。请观察下面的代码片段:

class A:
    def add(self,a,b):
        s = a+b
        print(s)
    def add(self,a,b,c):
        s = a+b+c
        print(s) 

这里我们有一个名为 A 的类,其中包含两个方法。这两个方法都使用相同的名称,即 add(),但是两个方法的参数数量不同。一个方法有两个参数,而另一个方法有三个参数。这里,add() 方法被重载了

现在让我们看看在 Python 中执行时的输出。

class A:
    def add(self,a,b):
        s = a+b
        print(s)
    def add(self,a,b,c):
        s = a+b+c
        print(s)

ob = A()
ob.add(10,20)
 
Traceback (most recent call last):
  File "poly_ex.py", line 73, in 
    ob.add(10,20)
TypeError: add() missing 1 required positional argument: 'c'

这里发生的情况是,由于两个方法具有相同的名称,Python 会保留类中最后定义的方法,并在调用函数时检查最后定义的方法,即带有三个参数的方法。带有两个参数的 add() 方法在类中就好像什么都没有一样。这意味着 Python 不支持方法重载,与其他 OOP 语言不同。

注意:Python 不支持方法重载。

但是,幸运的是,我们有 Pythonic 的方法可以在 Python 中实现方法重载。

class A:
    def add(self,a = None,b = None, c = None):
        if a!=None and b!=None and c!=None:
         print (a+b+c)
        else:
         print(a+b)
    
ob = A()
ob.add(10,20)
ob.add(30,40,50)
ob.add('Programming','Tutorials') 

输出

30
120
ProgrammingTutorials

上面的代码片段展示了如何通过一些技巧在 Python 中实现方法重载。在这里,我们可以通过三种不同的方式调用 add() 方法。

  • ob.add(10,20)
  • ob.add(30,40,50)
  • ob.add(‘Programming’,’Tutorials’)

再次提醒您,这不是实现方法重载的理想方式,因此在 Python 中不能称为方法重载。但不知何故,我们还是在 Python 中实现了方法重载。

Python 中的方法重写是什么?

重写,在面向对象编程语言中,是一个支持运行时多态的重要概念。当子类中的一个方法与父类中的一个方法具有相同的名称、相同的参数数量相同的返回类型(签名)时,该方法就被称为被重写。方法重写的主要好处是子类可以在不更改父类方法的情况下,为继承的方法提供自己的特定实现。

因此,在方法重写中,我们有两个方法:一个在父类中,称为被重写方法;一个在子类中,称为重写方法。两个方法具有相同的名称和相同的签名。

方法重写示例

为了更容易理解,请看一个包含两个类的简单示例:父类 - Mom,子类 - Daughter

#Parent Class 
class Mom:
    def dance(self):
        print("Mom is dancing")
    def cook(self):
        print("Mom is cooking")    

#Child class
class Daughter(Mom):
        pass
   
d = Daughter()
d.dance()
d.cook() 

输出

Mom is dancing
Mom is cooking

父类包含两个方法:dance()cook(),它们会打印一些消息。目前,我们将 Daughter 类留空,没有任何方法或属性,但是我们创建了一个 Daughter 类的对象,并用它来调用 Mom 中的方法。由于 Daughter 继承自 Mom,它可以访问 Mom 类中的方法并产生输出。

现在,让我们创建重写 dance() 方法的子类 Daughter。

#parent class
class Mom:
    #overridden method
    def dance(self):
        print("Mom is dancing")
    def cook(self):
        print("Mom is cooking")    

#child class
class Daughter(Mom):
    #overriding method
    def dance(self):
        print("Daughter is dancing ")

d = Daughter()
d.dance()
d.cook() 

输出

Daughter is dancing
Mom is cooking

现在,当我们调用对象 d 时,将执行修改后的 dance() 方法,这在输出中得到了清晰的体现。因此,我们可以得出结论:daughter 类重写了 Mom 类的方法。