Python 简明教程 — 21,Python 继承与多态

微信公众号:码农充电站pro

个人主页: https://codeshellme.github.io

程序不是年轻的专利,但是,它属于年轻。

目录

我们已经知道 封装继承多态 是面向对象的三大特征,面向对象语言都会提供这些机制。

1,封装

在这一节介绍类的 私有属性和方法 的时候,我们已经讲到过 封装

封装 就是在设计一个类的时候,只允许使用者访问他需要的方法,将复杂的,没有必要让使用者知道的方法隐藏起来。这样,使用者只需关注他需要的东西,为其屏蔽了复杂性。

私有性 就是实现 封装 的一种手段,这样,类的设计者就可以控制类中的哪些属性和方法可以被使用者访问到。一般,类中的属性,和一些复杂的方法都不会暴露给使用者。

由于前边的章节介绍过封装,这里就不再举例说明了。

2,继承

通过 继承 的机制,可使得 子类 轻松的拥有 父类 中的 属性和方法继承 也是一种 代码复用 的方式。

Python 支持类的继承, 继承的类 叫做 子类 或者 派生类被继承的类 叫做 父类基类

继承的语法如下:

class 子类名(父类名):
    pass

子类名 后边的括号中,写入要继承的父类。

object

在Python 的继承体系中, object 是最顶层类,它是所有类的父类。在定义一个类时,如果没有继承任何类,会默认继承 object 类。如下两种定义方式是等价的:

# 没有显示继承任何类,默认继承 object
class A1:
    pass

# 显示继承 object
class A2(object):
    pass

每个类中都有一个 mro 方法,该方法可以打印类的继承关系(顺序)。我们来查看 A1A2 的继承关系:

>>> A1.mro()
[, ]
>>>
>>> A2.mro()
[, ]

可见这两个类都继承了 object 类。

继承中的 __init__ 方法

当一个子类继承一个父类时,如果子类中没有定义 __init__ ,在创建子类的对象时,会调用父类的 __init__ 方法,如下:

#! /usr/bin/env python3

class A(object):

    def __init__(self):
        print('A.__init__')

class B(A):
    pass

以上代码中, B 继承了 AA 中有 __init__ 方法, B 中没有 __init__ 方法,创建类 B 的对象 b

>>> b = B()
A.__init__

可见 A 中的 __init__ 被执行了。

方法覆盖

如果类 B 中也定义了 __init__ 方法,那么,就只会执行 B 中的 __init__ 方法,而不会执行 A 中的 __init__ 方法:

#! /usr/bin/env python3

class A(object):

    def __init__(self):
        print('A.__init__')

class B(A):

    def __init__(self):
        print('B.__init__')

此时创建 B 的对象 b

>>> b = B()
B.__init__

可见,此时只执行了 B 中的 __init__ 方法。这其实是 方法覆盖 的原因,因为 子类 中的 __init__父类 中的 __init__ 的参数列表一样,此时,子类中的方法覆盖了父类中的方法,所以创建对象 b 时,只会执行 B 中的 __init__ 方法。

当发生继承关系(即一个子类继承一个父类)时,如果子类中的一个方法与父类中的一个方法 一模一样 (即方法名相同,参数列表也相同),这种情况就是 方法覆盖 (子类中的方法会覆盖父类中的方法)。

方法重载

方法名参数列表 都一样时会发生 方法覆盖 ;当 方法名 一样, 参数列表 不一样时,会发生 方法重载

在单个类中,代码如下:

#! /usr/bin/env python3

class A(object):

    def __init__(self):
        print('A.__init__')

    def test(self):
        print('test...')

    def test(self, i):
        print('test... i:%s' % i)

A 中的两个 test 方法, 方法名 相同, 参数列表 不同。

其实这种情况在 JavaC++ 是允许的,就是 方法重载 。而在Python 中,虽然在类中这样写不会报错,但实际上,下面的 test(self, i) 已经把上面的 test(self) 给覆盖掉了。创建出来的对象只能调用 test(self, i) ,而 test(self) 是不存在的。

示例:

>>> a = A()       # 创建 A 的对象 a
A.__init__
>>>
>>> a.test(123)   # 可以调用 test(self, i) 方法
test... i:123
>>>
>>> a.test()      # 调用 test(self) 发生异常
Traceback (most recent call last):
  File "", line 1, in 
TypeError: test() missing 1 required positional argument: 'i'

在继承关系中,代码如下:

#! /usr/bin/env python3

class A(object):

    def __init__(self):
        print('A.__init__')

    def test(self):
        print('test...')

class B(A):

    def __init__(self):
        print('B.__init__')

    def test(self, i):
        print('test... i:%s' % i)

上面代码中 B 继承了 ABA 中都有一个名为 test 的方法,但是 参数列表 不同。

这种情况跟在单个类中的情况是一样的,在类 B 中, test(self, i) 会覆盖A 中的 test(self) ,类 B 的对象只能调用 test(self, i) ,而不能调用 test(self)

示例:

>>> b = B()        # 创建 B 的对象
B.__init__
>>> 
>>> b.test(123)    # 可以调用 test(self, i)  方法
test... i:123
>>>
>>> b.test()       # 调用 test(self) 方法,出现异常
Traceback (most recent call last):
  File "", line 1, in 
TypeError: test() missing 1 required positional argument: 'i'

super() 方法

super() 方法用于调用父类中的方法。

示例代码:

#! /usr/bin/env python3

class A(object):

    def __init__(self):
        print('A.__init__')

    def test(self):
        print('class_A test...')

class B(A):

    def __init__(self):
        print('B.__init__')
        super().__init__()     # 调用父类中的构造方法

    def test(self, i):
        print('class_B test... i:%s' % i)
        super().test()         # 调用父类中的 test 方法

演示:

>>> b = B()          # 创建 B 的对象
B.__init__           # 调用 B 的构造方法
A.__init__           # 调用 A 的构造方法
>>>
>>> b.test(123)      # 调用 B 中的 test 方法 
class_B test... i:123
class_A test...      # 执行 A 中的 test 方法

is-a 关系

一个子类的对象,同时也是一个父类的对象,这叫做 is-a 关系。但是一个父类的对象,不一定是一个子类的对象。

这很好理解,就像,猫一定是动物,但动物不一定是猫。

我们可以使用 isinstance() 函数来判断一个对象是否是一个类的实例。

比如我们有如下两个类, Cat 继承了 Animal

#! /usr/bin/env python3

class Animal(object):
    pass

class Cat(Animal):
    pass

来看下对象和类之间的从属关系:

>>> a = Animal()           # 创建 Animal 的对象
>>> c = Cat()              # 创建 Cat 的对象
>>> 
>>> isinstance(a, Animal)  # a 一定是 Animal 的实例
True
>>> isinstance(c, Cat)     # c 一定是 Cat 的实例
True
>>> 
>>> isinstance(c, Animal)  # Cat 继承了 Animal,所以 c 也是 Animal 的实例
True
>>> isinstance(a, Cat)     # 但 a 不是 Cat 的实例
False

3,多继承

多继承 就是一个子类同时继承多个父类,这样,这个子类就同时拥有了多个父类的特性。

C++ 语言中允许多继承,但由于多继承会使得类的继承关系变得复杂。因此,到了Java 中,就禁止了多继承的方式,取而代之的是,在Java 中允许同时继承多个 接口

Python 中也允许多继承,语法如下:

# 括号中可以写多个父类
class 子类名(父类1, 父类2, ...):
    pass

我们构造一个如下的继承关系:

代码如下:

#! /usr/bin/env python3

class A(object):
    def test(self):
        print('class_A test...')

class B(A):
    def test(self):
        print('class_B test...') 

class C(A):
    def test(self):
        print('class_C test...') 

class D(B, C):
    pass

ABC 中都有 test() 方法, D 中没有 test() 方法。

使用 D 类中的 mro() 方法查看继承关系:

>>> D.mro()
[, , , , ]

创建 D 的对象:

>>> d = D()

如果类 D 中有 test() 方法,那么 d.test() 肯定会调用 D 中的 test() 方法,这种情况很简单,不用多说。

当类 D 中没有 test() 方法时,而它继承的父类 BC 中都有 test() 方法,此时会调用哪个 test() 呢?

>>> d.test()
class_B test...

可以看到 d.test() 调用了类 B 中的 test() 方法。

实际上这种情况下,Python 解释器会根据 D.mro() 的输出结果来依次查找 test() 方法,即查找顺序是 D->B->C->A->object

所以 d.test() 调用了类 B 中的 test() 方法。

建议:

由于 多继承 会使类的继承关系变得复杂,所以并不提倡过多的使用 多继承

4,多态

多态 从字面上理解就是一个事物可以呈现多种状态。 继承 是多态的基础。

在上面的例子中,类 D 的对象 d 调用 test() 方法时,沿着 继承链D.mro() )查找合适的 test() 方法的过程,就是多态的表现过程。

比如,我们有以下几个类:

  • Animal :有一个 speak() 方法
  • Cat :继承 Animal 类,有自己的 speak() 方法
  • Dog :继承 Animal 类,有自己的 speak() 方法
  • Duck :继承 Animal 类,有自己的 speak() 方法

CatDogDuck 都属于动物,因此都继承 Animal ,代码如下:

#! /usr/bin/env python3

class Animal(object):
    def speak(self):
        print('动物会说话...')

class Cat(Animal):
    def speak(self):
        print('喵喵...') 

class Dog(Animal):
    def speak(self):
        print('汪汪...') 

class Duck(Animal):
    def speak(self):
        print('嘎嘎...') 

def animal_speak(animal):
    animal.speak()

我们还定义了一个 animal_speak 函数,它接受一个参数 animal ,在函数内,调用了 speak() 方法。

实际上,这种情况下,我们调用 animal_speak 函数时,可以为它传递 Animal 类型的对象,以及任何的 Animal 子类的对象。

传递 Animal 的对象时,调用了 Animal 类中的 speak()

>>> animal_speak(Animal())
动物会说话...

传递 Cat 的对象时,调用了 Cat 类中的 speak()

>>> animal_speak(Cat())
喵喵...

传递 Dog 的对象时,调用了 Dog 类中的 speak()

>>> animal_speak(Dog())
汪汪...

传递 Duck 的对象时,调用了 Duck 类中的 speak()

>>> animal_speak(Duck())
嘎嘎...

可以看到,我们可以给 animal_speak() 函数传递 多种不同类型 的对象,为 animal_speak() 函数传递不同类型的参数,输出了不同的结果,这就是 多态

5,鸭子类型

静态类型 语言中,有严格的类型判断,上面的 animal_speak() 函数的参数只能传递 Animal 及其 子类 的对象。

而Python 属于 动态类型 语言,不会进行严格的类型判断。

因此,我们不仅可以为 animal_speak() 函数传递 Animal 及其 子类 的对象,还可以传递其它与 Animal 类毫不相关的类的对象,只要该类中有 speak() 方法就行。

这种特性,在Python 中被叫做 鸭子类型 ,意思就是, 只要一个事物走起来像鸭子,叫起来像鸭子,那么它就是鸭子,即使它不是真正的鸭子

从代码上来说,只要一个类中有 speak() 方法,那么就可以将该类的对象传递给 animal_speak() 函数。

比如,有一个鼓类 Drum ,其中有一个函数 speak()

class Drum(object):
    def speak(self):
        print('咚咚...')

那么,类 Drum 的对象也可以传递给 animal_speak() 函数,即使 DrumAnimal 类毫不相关:

>>> animal_speak(Drum())
咚咚...

从另一个角度来考虑,实际上Python 函数中的参数,并没有标明参数的类型。在 animal_speak() 函数中,我们只是将参数叫做了 animal 而已,因此我们就认为 animal_speak() 函数应该接受Animal 类及其子类的对象,其实这仅仅只是我们认为的而已。

计算机并不知道 animal 的含义,如果我们将原来的 animal_speak() 函数:

def animal_speak(animal):
    animal.speak()

改写成:

def animal_speak(a):
    a.speak()

实际上,我们知道,这两个函数并没有任何区别。因此,参数 a 可以是任意的类型,只要 a 中有 speak() 方法就行。这就是Python 能够表现出 鸭子特性 的原因。

(完。)