第四章 面向对象编程 -- 继承、接口

第零章 学前准备
第一章 数据结构 – 基本数据类型
第一章 数据结构 – 字符串
第一章 数据结构 – 列表、元组和切片
第一章 数据结构 – 字典
第一章 数据结构 – 集合
第一章 – 数组、队列、枚举
第一章 数据结构 – 序列分类
第二章 控制流程
第三章 函数也是对象 – 函数定义以及参数
第三章 函数也是对象 – 高阶函数以及装饰器
第三章 函数也是对象 – lambda 表达式、可调用函数及内置函数
第四章 面向对象编程 – 自定义类、属性、方法和函数
第四章 面向对象编程–魔术方法1
第四章 面向对象编程 – 魔术方法2
第四章 面向对象编程 – 可迭代的对象、迭代器和生成器
第四章 面向对象编程 – 继承、接口


第四章 第四章 面向对象编程 – 继承、接口

4.7 继承

一个类继承另一个类时,它将自动获得另一个类的所有属性和方法;原有的类称为父类,而新类称为子类。子类继承了其父类的所有属性和方法,同时还可以定义自己的属性和方法。用户自定义类默认继承了基类 object

如果你想重写父类的方法,只需要定义与父类同名的方法而已。

python 里面不像 javac++ ,支持不同函数签名的函数。 python 中不允许两个函数同名,如果有同名函数,后面的会覆盖前面的函数。

4.7.1 多重继承

python 支持多重继承,任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为"菱形问题"。

在这里插入图片描述

注意, BC 都实现了 pong 方法,二者之间唯一的区别是, C.pong 方法输出的是大写的 PONG 。在 D 的实例上调用 d.pong() 方法的话,运行的是哪个 pong 方法呢?在 C++ 中,程序员必须使用类名限定方法调用来避免这种歧义。 Python 也能这么做。

Python 能区分 d.pong() 调用的是哪个方法,是因为 Python 会按照特定的顺序遍历继承图。这个顺序叫方法解析顺序( Method Resolution Order,MRO )。类都有一个名为 __mro__ 的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到 object 类。 D 类的 __mro__ 属性如下: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

import doctest


class A:
    def ping(self):
        print('ping:', self)


class B(A):
    @property
    def name(self):
        return self.__class__.__name__

    def pong(self):
        print('pong:', self)


class C(A):
    def pong(self):
        print('PONG:', self)


class D(B, C):
    """
    >>> d=D()

    直接调用d.pong()运行的是B类中的版本。
    >>> d.pong() # doctest:+ELLIPSIS
    pong: <__main__.D object at 0x...>

    超类中的方法都可以直接调用,此时要把实例作为显式参数传入。
    >>> C.pong(d) # doctest:+ELLIPSIS
    PONG: <__main__.D object at 0x...>

    D类的__mro__属性如下
    >>> D.__mro__
    (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

    直接调用d.ping()运行的是D类中的版本。
    >>> d.ping() # doctest:+ELLIPSIS
    ping: <__main__.D object at 0x...>
    D
    post-ping: <__main__.D object at 0x...>
    """

    def ping(self):
        super().ping()
        print(super().name)
        print('post-ping:', self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)


if __name__ == "__main__":

    doctest.testmod()

4.7.2 使用 super 调用超类方法

super([type[, object-or-type]]) 返回一个代理对象,它会将方法调用委托给 type 的父类或兄弟类。 这对于访问已在类中被重载的继承方法很有用。 object-or-type 确定用于搜索的 method resolution order 。 搜索会从 type 之后的类开始。

举例来说,如果 object-or-type__mro__D -> B -> C -> A -> object 并且 type 的值为 B,则 super() 将会搜索 C -> A -> object

object-or-type__mro__ 属性列出了 getattr()super() 所共同使用的方法解析搜索顺序。 该属性是动态的,可以在任何继承层级结构发生更新的时候被改变。

如果省略第二个参数,则返回的超类对象是未绑定的。 如果第二个参数为一个对象,则 isinstance(obj, type) 必须为真值。 如果第二个参数为一个类型,则 issubclass(type2, type) 必须为真值(这适用于类方法)。

super 有两个典型用例:

  • 在具有单继承的类层级结构中,super 可用来引用父类而不必显式地指定它们的名称,从而令代码更易维护。 这种用法与其他编程语言中 super 的用法非常相似。
  • 第二个用例是在动态执行环境中支持协作多重继承。 此用例为 Python 所独有而不存在于静态编码语言或仅支持单继承的语言当中。 这使用实现“菱形图”成为可能,即有多个基类实现相同的方法。 好的设计强制要求这样的方法在每个情况下都具有相同的调用签名(因为调用顺序是在运行时确定的,也因为这个顺序要适应类层级结构的更改,还因为这个顺序可能包括在运行时之前未知的兄弟类)。

对于以上两个用例,典型的超类调用看起来是这样的:

class C(B):
    def method(self, arg):
        super().method(arg)    # This does the same thing as:super(C, self).method(arg)

除了方法查找之外, super() 也可用于属性查找。 属性查找的一个场景就是在上级或同级类中调用描述器。

4.8 接口:从协议到抽象基类(Abstract Base Class,ABC)

4.8.1 鸭子类型和白鹅类型

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。对象的类型无关紧要,只要实现了特定的协议即可。忽略对象真正的类型,转而关注对象有没有实现所需的方法、签名和语义。 一个用户定义的类型,不需要真正的继承自抽象基类,但是却可以当作其子类一样来使用。比如用户实现了序列协议,就可以当作内置序列类型来用,对其使用 len() 等函数,调用 __len__() 等用于内置类型的方法。比如用户实现了 __getitem__ 方法, Python 就可以直接去迭代这个类型。 Python 内置库和第三方的库,虽然是针对 Python 的类型设计的,但是都可以直接用于用户自定义的类型上。

而白鹅类型与此恰好相反。

使用抽象基类明确声明接口,子类显示地继承抽象基类,抽象基类会检查子类是否符合接口定义。白鹅类型和鸭子类型相比,一些直接继承自抽象基类的接口是可以拿来即用的,但是子类为了经过抽象基类的接口检查,必须实现一些接口,虽然这些接口你可能不需要。

from collections import abc


class Struggle:
    def __len__(self):
        return 23


# 只要实现了__len__方法,就是Sized的子类
print(isinstance(Struggle(), abc.Sized))
True

4.8.2 定义并使用抽象基类

Tombola 抽象基类有四个方法,其中两个是抽象方法。

  • load(...):把元素放入容器。
  • pick(...):从容器中随机拿出一个元素,返回选中的元素。

另外两个是具体方法。

  • loaded():如果容器中至少有一个元素,返回True
  • inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。



4.8.3 Tombola的虚拟子类

白鹅类型的一个基本特性(也是值得用水禽来命名的原因):即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而 Python 会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕获。

注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且 issubclassisinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现所需的全部方法。

import abc
import random


class Tombola(abc.ABC):  # ➊ 自己定义的抽象基类要继承abc.ABC。
    @abc.abstractmethod
    def load(self, iterable):  # ➋ 抽象方法使用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串。与其他方法描述符一起使用时,abstractmethod()应该放在最里层
        """从可迭代对象中添加元素。"""
    @abc.abstractmethod
    def pick(self):  # ➌ 根据文档字符串,如果没有元素可选,应该抛出LookupError。
    """随机删除元素,然后将其返回。
       如果实例为空,这个方法应该抛出`LookupError`。
    """

    def loaded(self):  # ➍ 抽象基类可以包含具体方法
    """如果至少有一个元素,返回`True`,否则返回`False`。"""
    return bool(self.inspect())  # ➎ 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具体方法、抽象方法或特性)。

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:  # ➏ 我们不知道具体子类如何存储元素,不过为了得到inspect的结果,我们可以不断调用.pick()方法,把Tombola清空
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)  # ➐ 然后再使用load()把所有元素放回去。
        return tuple(sorted(items))


class BingoCage(Tombola):  # ➊ 明确指定BingoCage类扩展Tombola类

    def __init__(self, items):
        # ➋ 假设我们将在线上游戏中使用这个。random.SystemRandom使用os.urandom(...)函数实现random API。根据os模块的文档,os.urandom(...)函数生成“适合用于加密”的随机字节序列。
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)  # ➌ 委托.load(...)方法实现初始加载。

    def load(self, items):
        self._items.extend(items)
        # ➍ 没有使用random.shuffle()函数,而是使用SystemRandom实例的.shuffle()方法。
        self._randomizer.shuffle(self._items)

    def pick(self):
       try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')

    def __call__(self):  # ➏ bingoCage.pick()的快捷方式是bingoCage()
       self.pick()


class LotteryBlower(Tombola):
    def __init__(self, iterable):
        self._balls = list(iterable) # ➊ 初始化方法接受任何可迭代对象:把参数构建成列表。

    def load(self, iterable):
        self._balls.extend(iterable)

    def pick(self):
        try:
            position = random.randrange(len(self._balls))  # ➋ 如果范围为空,random.randrange(...)函数抛出ValueError,为了兼容Tombola,我们捕获它,抛出LookupError。
        except ValueError:
            raise LookupError('pick from empty LotteryBlower')
        return self._balls.pop(position) # ➌ 否则,从self._balls中取出随机选中的元素。

    def loaded(self): # ➍ 覆盖loaded方法,避免调用inspect方法。我们可以直接处理self._balls而不必构建整个有序元组,从而提升速度。
       return bool(self._balls)

    def inspect(self): # ➎ 使用一行代码覆盖inspect方法。
        return tuple(sorted(self._balls))


@Tombola.register  # ➊ 把Tombolist注册为Tombola的虚拟子类。
class TomboList(list):  # ➋  Tombolist扩展list。
    def pick(self):
        if self:  # ➌ Tombolist从list中继承__bool__方法,列表不为空时返回True。
            position = randrange(len(self))
            return self.pop(position)  # ➍ pick调用继承自list的self.pop方法,传入一个随机的元素索引。
        else:
            raise LookupError('pop from empty TomboList')
    load = list.extend  # ➎ Tombolist.load与list.extend一样。
    def loaded(self): 
        return bool(self)  # ➏ loaded方法委托bool函数。
    def inspect(self):
        return tuple(sorted(self))
# Tombola.register(TomboList)  # ➐ 如果是Python 3.3或之前的版本,不能把.register当作类装饰器使用,必须使用标准的调用句法。

猜你喜欢

转载自blog.csdn.net/qq_31654025/article/details/128499641