Python中的继承、抽象基类和接口

Python中的继承、抽象基类和接口[Python常见问题]

先一句话总结Python中继承、抽象基类和接口三者之间的关系:Python中的接口机制可通过抽象基类实现,接口的实现有赖于继承机制。

一、继承

继承是面向对象编程语言的三大特性之一(其他两个是封装、多态),所谓继承是指子类自动具有父类所定义的方法和属性,而无需子类再重复定义同名的方法或属性,因此继承的最大优势之一是可以提高代码的复用程度。

1. 常见数列案例

这里以高中数学中一个重要的概念——数列来简介Python的继承概念。数列是一组数值组成的序列,该序列中的每一个值都取决于数列的前一项或多项,例如:

  • 对于等差数列,数列中从第二项开始,每一项都由前一项加上一个固定的常量得到;
  • 对于等比数列,数列中从第二项开始,每一项都由前一项乘上一个固定的常量得到;
  • 对于斐波那契数列,数列中从第三项开始,每一项都有其前两项之和相加得到。

如果现在需要使用面向对象特性对上述各个不同类型的数列进行代码抽象,则可以想到三个数列类必然都支持下列类似功能的方法:

  • 初始化方法:用于初始化数列的前若干项;
  • 遍历支持方法:可以支持以可迭代的方式遍历出数列的项;
  • 数列项生成方法:按照一定的规则根据前若干项生成任意项。

如果不采用继承的方式,则最终实现的各数列类必然代码重复度很高。

2. 常见数列实现

针对上述讨论,下面考虑使用继承实现各个数列类:

  • 首先,定义一个通用数列父类Progression,在其中实现数列的共有方法及实用方法;
  • 然后,继承Progression类再根据数列通项生成规则分别在等差、等比、斐波那契数列中重写父类方法或定义全新方法。

数列基类

在数列基类中:

  • __init__初始化方法接收两个参数,start用以指定数列第一项的值,num用以指定默认打印的数列项数,分别用于初始化_current_num的值;
  • _advance方法用于按照数列通项规则生成任意项;
  • __iter____next__方法用以支持Python的迭代器协议(具体请见Python中for循环运行机制探究以及可迭代对象、迭代器详解),用于数列的遍历;
  • __str__方法用于将数列对象转换为列表,并返回该列表的字符串表示形式。
class Progression:
    """数列基类"""

    def __init__(self, start=0, num=10):
        """
        将当前数列的第一项初始化为0
        :param start: 数列第一项,默认为0
        :param num: 打印数列时的默认显示项数
        """
        self._current = start
        self._num = num

    def _advance(self):
        """用于根据数列前若干项进行任意项的生成,该方法应该被子类重写"""
        self._current += 1

    def __next__(self):
        """迭代器协议方法,返回数列中的下一项,当已至数列最后一项则抛出StopIteration异常"""
        if self._num > 0:
            ans = self._current
            self._advance()
            self._num -= 1
            return ans
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器协议方法,返回对象自身"""
        return self

    def __str__(self):
        """返回对象的字符串表示形式"""
        return str(list(self))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

等差数列

在等差数列的实现中,由于继承了Progression类,所以:

  • __init__方法中调用了父类初始化方法,从而对继承自父类的_current以及默认打印的数列项数_num进行了初始化,另外还对该类特有的等差数列常量_increment进行了初始化;
  • _advance方法按照等差数列通项规则对父类同名方法进行了重写;
  • __next____iter____str__方法继承自Progression类,无需重复编写代码。
class ArithmeticProgression(Progression):
    """等差数列"""

    def __init__(self, start=0, increment=1, num=10):
        """
        创建一个新的等差数列
        :param increment: 等差常量,默认为1
        :param start: 数列首项,默认为0
        :param num: 打印数列时的默认显示项数
        """
        super().__init__(start=start, num=num)
        self._increment = increment

    def _advance(self):  # 重写父类同名方法
        """根据等差数列通项规则,生成任意项"""
        self._current += self._increment
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

等比数列

在等比数列的实现中,由于继承了Progression类,所以:

  • __init__方法中调用了父类初始化方法,从而对继承自父类的_current以及默认打印的数列项数_num进行了初始化,另外还对该类特有的等比数列常量_base进行了初始化;
  • _advance方法按照等比数列通项规则对父类同名方法进行了重写;
  • __next____iter____str__方法继承自Progression类,无需重复编写代码。
class GeometricProgression(Progression):
    """等比数列"""

    def __init__(self, start=1, num=10, base=2):
        """
        创建一个新的等比数列
        :param base: 等比常量,默认值为2
        :param start: 数列首项,默认为1
        :param num: 打印数列时的默认显示项数
        """
        super().__init__(start=start, num=num)
        self._base = base

    def _advance(self):
        """根据等比数列通项规则,生成任意项"""
        self._current *= self._base
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

斐波那契数列

在斐波那契数列的实现中,由于继承了Progression类,所以:

  • __init__方法中调用了父类初始化方法,从而对继承自父类的_current以及默认打印的数列项数_num进行了初始化,另外还对该类特有的假想第0项_prev进行了初始化;
  • _advance方法按照斐波那契数列通项规则对父类同名方法进行了重写;
  • __next____iter____str__方法继承自Progression类,无需重复编写代码。
class FibonacciProgression(Progression):
    """斐波那契数列"""

    def __init__(self, first=0, second=1, num=10):
        """
        创建一个新的斐波那契数列
        :param first: 数列第一项,默认为0
        :param second: 数列第二项,默认为1
        :param num: 打印数列时的默认显示项数
        """
        super().__init__(start=first, num=num)
        self._prev = second - first  # 假想在第一项之前存在的第零项

    def _advance(self):
        """根据斐波那契数列通项规则,生成任意项"""
        self._prev, self._current = self._current, self._prev + self._current
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

下面是对上述几个数列实现类的测试结果:

if __name__ == "__main__":
    print("默认数列Progression:")
    print(Progression(num=5), end="
"*2)  # [0, 1, 2, 3, 4]
    
    print("等差数列ArithmeticProgression:")
    print(ArithmeticProgression(start=10, increment=3, num=7), end="
"*2)  # [10, 13, 16, 19, 22, 25, 28]
    
    print("等比数列GeometricProgression:")
    print(GeometricProgression(start=4, base=3, num=9), end="
"*2)  # [4, 12, 36, 108, 324, 972, 2916, 8748, 26244]
    
    print("斐波那契数列FibonacciProgression:")
    print(FibonacciProgression(first=2, num=12))  # [2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123, 199]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

二、抽象基类

仔细分析上述代码可知,基类Progression仅是为了作为ArithmeticProgressionGeometricProgression以及FibonacciProgression的基类,虽然可以通过实例化Progression得到一个对象,但这意义不大,因为其仅是ArithmeticProgression的一种特殊情况,即第一项为0,等差常量为1的等差数列。

在支持面向对象编程范式的语言中,对Progression这种仅作为基类用于指定多个子类所需实现的方法的类,有一个专门的术语——抽象基类。

在Python3中想要定义一个抽象基类,可通过如下步骤实现:

  • 在定义抽象基类前,从模块abc中导入类ABCMeta和方法abstractmethod
  • 在定义抽象基类时:
    • 在抽象基类名后指定metaclassABCMeta
    • 在抽象基类需被子类继承后实现的方法(一般称为抽象方法)前使用@abstractmethod

例如:如前所述,对于Progression方法,按照上述流程将其定义为抽象基类的代码如下:

from abc import ABCMeta, abstractmethod


class Progression(metaclass=ABCMeta):
    """数列基类"""

    def __init__(self, start=0, num=10):
        """
        将当前数列的第一项初始化为0
        :param start: 数列第一项,默认为0
        :param num: 打印数列时的默认显示项数
        """
        self._current = start
        self._num = num

    @abstractmethod
    def _advance(self):
        """用于根据数列前若干项进行任意项的生成,该方法应该被子类重写"""

    def __next__(self):
        """迭代器协议方法,返回数列中的下一项,当已至数列最后一项则抛出StopIteration异常"""
        if self._num > 0:
            ans = self._current
            self._advance()
            self._num -= 1
            return ans
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器协议方法,返回对象自身"""
        return self

    def __str__(self):
        """返回对象的字符串表示形式"""
        return str(list(self))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

可以看出,上述代码中我们将_advance定义成了抽象方法,因为该方法一方面在所有子类中都必须存在,另一方面该方法在所有子类中的实现又都完全不同。

需要指出的是,对于抽象基类(如:Progression)不能直接对其通过实例化创建对象,否则会报这样的错误:TypeError: Can"t instantiate abstract class Progression with abstract methods _advance

三、接口

接口是一种编程机制,这种机制可以确保不同的代码编写者可以:

  • 遵循相同的代码签名,如:方法名称(_advance)、参数、返回值;
  • 使用不同的算法实现具体代码,如:根据等差、等比、斐波那契数列的通项生成规则实现_advance方法。

接口机制的好处在于,可以:

  • 实现不同代码编写者之间的协作,如:应用架构者可以对整体框架做搭建,而将具体实现留给实施人员,这有点像你的老板一般会告诉你要做根据手头的资源某几件事情以及期望结果是什么,而你和你的同事需要通过一定的过程努力将每一件事具体实施好;
  • 实现代码间的松耦合,各个接口的实现人员无需了解其他人员对接口的内部具体实现。

Python中,对于接口的具体实现,只要在子类中继承抽象基类,然后实现其中的所有抽象方法即可。

下面还是以上述的数列类为例演示接口实现的过程:

from abc import ABCMeta, abstractmethod


class Progression(metaclass=ABCMeta):
    """数列基类"""

    def __init__(self, start=0, num=10):
        """
        将当前数列的第一项初始化为0
        :param start: 数列第一项,默认为0
        :param num: 打印数列时的默认显示项数
        """
        self._current = start
        self._num = num

    @abstractmethod
    def _advance(self):
        """用于根据数列前若干项进行任意项的生成,该方法应该被子类重写"""

    def __next__(self):
        """迭代器协议方法,返回数列中的下一项,当已至数列最后一项则抛出StopIteration异常"""
        if self._num > 0:
            ans = self._current
            self._advance()
            self._num -= 1
            return ans
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器协议方法,返回对象自身"""
        return self

    def __str__(self):
        """返回对象的字符串表示形式"""
        return str(list(self))


class FibonacciProgression(Progression):
    """斐波那契数列"""

    def __init__(self, first=0, num=10, second=1):
        """
        创建一个新的斐波那契数列
        :param first: 数列第一项,默认为0
        :param second: 数列第二项,默认为1
        :param num: 打印数列时的默认显示项数
        """
        super().__init__(start=first, num=num)
        self._prev = second - first  # 假想在第一项之前存在的第零项

    def _advance(self):
        """根据斐波那契数列通项规则,生成任意项"""
        self._prev, self._current = self._current, self._prev + self._current


if __name__ == "__main__":

    print(FibonacciProgression(first=2, num=12))  # [2, 1, 3, 4, 7, 11, 18, 29, 47, 76, 123, 1



本文首发于python黑洞网,博客园同步更新

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » Python中的继承、抽象基类和接口