一、 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 模块中的类, 例如 UserDict
、UserList
和 UserString
。这些类做了特殊设计,因此易于扩展。
如果不子类化 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),如下所示

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