Python深入-11-继承

一、 super() 函数

坚持使用内置函数super()是确保面向对象的 Python 程序可维护性的基本要求。
子类中覆盖超类的方法通常要调用超类中相应的方法。

class LastUpdatedOrderedDict(OrderedDict):

    """按照更新顺序存储项"""
    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        self.move_to_end(key)

为达目的,LastUpdatedOrderedDict 覆盖 __setitem__ 方法,做了以下两件事。

  • 通过 super().setitem 调用超类中对应的方法,插入或更新键–值对。

  • 调用 self.move_to_end,确保最后更新的key出现在最后。

调用被覆盖的 __init__方法尤其重要,可以让超类完成它负责的初始化任务。

def __init__(self, a, b) :
    super().__init__(a, b)
    ... # 其他初始化代码

你或许见过不使用super()函数而是直接在超类上调用方法的代码,如下所示。

class NotRecommended(OrderedDict):
    """这是一个反例!"""

    def __setitem__(self, key, value):
        OrderedDict.__setitem__(self, key, value)
        self.move_to_end(key)

这么做不是不可以,但是不推荐,原因有二。其一,硬编码了基类。OrderedDict 名称不仅出现在 class 语句中,还出现在__setitem__方法内。如果后来有人修改了 class 语句,更换了基类或者又 加了一个,那么说不定会忘记更新 __setitem__ 方法的主体,埋下 bug。其二,super 实现的逻辑能处理多重继承涉及的类层次结构。最后,看一下在 Python 2 中 如何调用 super,旧句法接受两个参数,这可以给我们一定启发。

class LastUpdatedOrderedDict(OrderedDict):
    """在Python 2和Python 3中都能正常运行"""

    def __setitem__(self, key, value):
        super(LastUpdatedOrderedDict, self).__setitem__(key, value)
        self.move_to_end(key)

现在,super 的两个参数都是可选的。Python 3 字节码编译器通过super()调用周围的上下文自动提供那两个参数。两个参数的作用如下。

  • type 从哪里开始搜索实现所需方法的超类。默认为super()调用所在的方法所属的类。

  • object_or_type接收方法调用的对象(调用实例方法时)或类(调用类方法时)。在实例方法中调用 super() 时,默 认为 self。

无论是我们自己还是编译器提供这两个参数,super() 调用都返回一个动态代理对象,在 type 参数指定 的超类中寻找一个方法(例如这里的 __setitem__),把它绑定到 object_or_type 上,因此调用那个 方法时不用显式传入接收者(self)。
在 Python 3 中,依然可以显式为 super() 提供第一个参数和第二个参数。但是,只有在特殊情况下才必须这么做,例如测试或调试时跳过部分 MRO,或者绕开不希望从超类得到的行为。

二、 子类化内置类型很麻烦

从 Python 2.2 开始,内置类型可以 子类化了,但是有一个重要的注意事项:内置类型(使用 C 语言编写)通常不调用用户定义的类覆盖的方 法。

关于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有制定官方规则。基本上,内置类型的方法不会调用子类覆盖的方法。例如,dict 的子类覆盖的 __getitem__() 方法不会被内置类型的get()方法调用

class DoppelDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, [value] * 2)  # DoppelDict.__setitem__ 方法把存入的值重复两次(只是为了提供易于观察的效果)。它把职责委托给了超类


if __name__ == "__main__":
    dd = DoppelDict(one=1)  # 继承自 dict 的 __init__ 方法显然忽略了我们覆盖的 __setitem__ 方法:'one' 的值没有重复
    print(dd)  # {'one': 1}

    # [] 运算符调用我们覆盖的 __setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即  # [2, 2]。
    dd['two'] = 2
    print(dd)  # {'one': 1, 'two': [2, 2]}
    # 继承自 dict 的 update 方法也没有使用我们覆盖的 __setitem__ 方法:'three' 的值没有重复
    dd.update(three=3)
    print(dd)  # {'one': 1, 'two': [2, 2], 'three': 3}

内置类型的这种行为违背了面向对象编程的一个基本原则:应始终从实例(self)所属的类开始搜索方 法,即使在超类实现的类中调用也是如此。这种行为叫作“晚期绑定”(late binding)。在 Alan Kay(因 Smalltalk 出名)看来,这是面向对象编程的关键功能:对于 x.method() 形式的调用,具体调用的方法必 须在运行时根据接收者 x 所属的类确定。在这种糟糕的局面中,__missing__ 方法却能按预期工作。
不只实例内部的调用有这个问题(self.get() 不调用 self.__getitem__(),内置类型的方法调用 的其他类的方法如果被覆盖了,则也不会被调用。

class AnswerDict(dict):
    def __getitem__(self, key):
        # 不管传入什么键,AnswerDict.__getitem__ 方法始终返回 42。
        return 42


if __name__ == "__main__":
    # ad 是 AnswerDict 实例,以 ('a', 'foo') 键–值对初始化
    ad = AnswerDict(a='foo')
    # ad['a'] 返回 42,符合预期。
    print(ad['a']) # 42

    # d 是 dict 实例,使用 ad 中的值更新 d。
    d = {}

    # dict.update 方法忽略了 AnswerDict.__getitem__ 方法
    d.update(ad)
    print(d['a']) # foo

直接子类化内置类型(例如 dict、list 或 str)容易出错,因为内置类型的方法通常会忽 略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承 collections 模块中的类, 例如 UserDictUserListUserString。这些类做了特殊设计,因此易于扩展。 
    如果不子类化 dict,而是子类化collections.UserDict,那么上面的2个例子中暴露的问题就 能迎刃而解了,如下所示:

import collections


class DoppelDict(collections.UserDict):
    def __setitem__(self, key, value):
        super().__setitem__(key, [value] * 2)  # DoppelDict.__setitem__ 方法把存入的值重复两次(只是为了提供易于观察的效果)。它把职责委托给了超类


if __name__ == "__main__":
    dd = DoppelDict(one=1)  
    print(dd)  # {'one': [1, 1]}

    # [] 运算符调用我们覆盖的 __setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即  # [2, 2]。
    dd['two'] = 2
    print(dd)  # {'one': [1, 1], 'two': [2, 2]}

    dd.update(three=3)
    print(dd)  # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}
import collections


class AnswerDict(collections.UserDict):
    def __getitem__(self, key):
        # 不管传入什么键,AnswerDict.__getitem__ 方法始终返回 42。
        return 42


if __name__ == "__main__":
    # ad 是 AnswerDict 实例,以 ('a', 'foo') 键–值对初始化
    ad = AnswerDict(a='foo')
    # ad['a'] 返回 42,符合预期。
    print(ad['a']) # 42

    # d 是 dict 实例,使用 ad 中的值更新 d。
    d = {}

    # dict.update 方法忽略了 AnswerDict.__getitem__ 方法
    d.update(ad)
    print(d['a']) # 42

总结:上面的两个问题例子,而且只影响直接继承内置类型的类。如果子类化使用 Python 编写的类(例如 UserDict 或 MutableMapping),则不会受此影 响。

三、 多重继承和方法解析顺序

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由超类实现同名方法时引起。我们称之为“菱 形问题”(diamond problem),如下所示

Python深入-11-继承
image.png


class Root:  # Root 不仅提供了 ping 方法和 pong 方法,为了输出更易读的表示形式,还实现了 __repr__ 方法。
    def ping(self):
        print(f'{self}.ping() in Root')

    def pong(self):
        print(f'{self}.pong() in Root')

    def __repr__(self):
        cls_name = type(self).__name__
        return f'<instance of {cls_name}>'


class A(Root):  # A 类中的 ping 方法和 pong 方法都调用了 super()。
    def ping(self):
        print(f'{self}.ping() in A')
        super().ping()

    def pong(self):
        print(f'{self}.pong() in A')
        super().pong()


class B(Root):  # B 类中只有 ping 方法调用了 super()。
    def ping(self):
        print(f'{self}.ping() in B')
        super().ping()

    def pong(self):
        print(f'{self}.pong() in B')


class Leaf(A, B):  # Leaf 类只实现了 ping 方法,而且该方法调用了 super()
    def ping(self):
        print(f'{self}.ping() in Leaf')
        super().ping()


if __name__ == "__main__":
    leaf1 = Leaf()  # leaf1 是 Leaf 实例。

    # 调用 leaf1.ping(),唤醒 Leaf、A、B 和 Root 中的 ping 方法,因为前 3 个类中的 ping 方法都调用了 super().ping()。
    leaf1.ping()
    print("====")
    # 调用 leaf1.pong(),唤醒继承树上 A 中的 pong,而它又调用 super.pong(),唤醒了 B.pong
    leaf1.pong()

"""
<instance of Leaf>.ping() in Leaf
<instance of Leaf>.ping() in A
<instance of Leaf>.ping() in B
<instance of Leaf>.ping() in Root
====
<instance of Leaf>.pong() in A
<instance of Leaf>.pong() in B
"""

每个类都有名为 __mro_的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直到object类。Leaf 类的 __mro__ 属性如下所示。 
    以实验中的 pong 方法为例。由于 Leaf 类没有覆盖该方法,因此调用 leaf1.pong() 唤醒的是 Leaf.__mro__ 中下一个类(A 类)实现的 pong 方法。A.pong 方法调用了 super().pong()。方法解析顺序的下一个类是 B,因此 B.pong 被唤醒。但是,因为 B.pong 方法没有调用super().pong(),所以唤醒过程到此结束。
方法解析顺序不仅考虑继承图,还考虑子类声明罗列超类的顺序。也就是说,在 diamond.py 文件中,如果把 Leaf 类声明为 Leaf(B, A),那么在 Leaf.__mro__中,B 类将出现在 A 类前 面。这会影响 ping 方法的唤醒顺序,而且 leaf1.pong() 将通过继承树唤醒 B.pong,但是不唤醒 A.pong 和 Root.pong,因为 B.pong 没有调用 super()。
调用 super() 的方法叫协作方法(cooperative method)。利用协作方法可以实现协作多重继承。这两个 术语就是字面意思,Python 中的多重继承涉及多个方法的协作。在 B 类中,ping 是协作方法,而 pong 则不是。

四、 混入类

 混入类在多重继承中会连同其他类一起被子类化。混入类不能作为具体类的唯一基类,因为混入类不为具 体对象提供全部功能,而是增加或定制子类或同级类的行为。  

在 Python 和 C++ 中,混入类只是一种约定,语言层面没有显式支持

不区分大小写的映射

import collections


# ❶ 这个辅助函数接受的 key 参数可以是任何类型,并会尝试返回 key.upper() 得到的结果;如果失败,
# 则返回未经修改的 key。
def _upper(key):
    try:
        return key.upper()
    except AttributeError:
        return key


# 这个混入类实现了映射的 4 个基本方法,总是调用 super(),传入尽量转换成大写形式的 key
class UpperCaseMixin:
    def __setitem__(self, key, item):
        super().__setitem__(_upper(key), item)

    def __getitem__(self, key):
        return super().__getitem__(_upper(key))

    def get(self, key, default=None):
        return super().get(_upper(key), default)

    def __contains__(self, key):
        return super().__contains__(_upper(key))

由于 UpperCaseMixin 中的每个方法都调用了 super(),因此这个混入类会依赖一个同级类,该类实现 或继承了签名相同的方法。为了让混入类发挥作用,在子类的方法解析顺序中,它要出现在其他类前面。也就是说,在类声明语句中,混入类必须出现在基类元组的第一位。  


原文始发于微信公众号(Python之家):Python深入-11-继承

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198379.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!