4.2 继承、多态与魔术方法


4.2 继承、多态与魔术方法

本节摘要:继承组织的是属性查找路径,MRO 是这条路径的精确描述;多态在 Python 里不是类型系统的恩赐,而是"实现了同名方法就行"的鸭子类型;魔术方法则是用户类型接入语言协议的插座。本节把这三件事一次讲清,并给出继承与组合的取舍准则。

MRO:查找路径的精确答案

多继承时,属性到底从哪个类来?Python 用 C3 线性化把继承网压成一条确定的顺序——方法解析顺序(MRO)。直接看:

class Animal: def speak(self): return "..." def intro(self): return f"我是动物,我说{self.speak()}" class Dog(Animal): def speak(self): return "汪汪" class Bird(Animal): def speak(self): return "啾啾" def intro(self): return "我是一只鸟" class DogBird(Dog, Bird): pass db = DogBird() db.speak() # '汪汪':MRO 里 Dog 在前 db.intro() # '我是动物,我说汪汪':intro 沿 MRO 先找到 Animal 的版本!

最后一个结果常让人意外:intro 不是取自更"近"的 Bird,而是按 MRO 找到 Animal。打印线性化序列一切明了:

>>> [c.__name__ for c in DogBird.__mro__] ['DogBird', 'Dog', 'Bird', 'Animal', 'object']

MRO 保证两条不变量:子类永远在父类前、声明顺序被尊重,且满足单调性。若继承图无法线性化,类定义当场报错(菱形图中某些病态组合会抛 TypeError: Cannot create a consistent MRO)——错误在类定义期而非调用期爆发,这是 MRO 设计的贴心之处。

菱形继承与 MRO

菱形继承与 MRO

super:沿 MRO 而行的接力棒

super() 理解成"调用父类"在单继承下没错,多继承下会误导。准确语义:沿实例的 MRO,找到当前类之后的下一个。菱形初始化的标准写法:

class Base: def __init__(self, **kw): print("Base ready") super().__init__(**kw) # object.__init__ 收尾 class LogMixin(Base): def __init__(self, *, verbose=False, **kw): self.verbose = verbose print("LogMixin ready") super().__init__(**kw) class Service(LogMixin, Base): def __init__(self, name, **kw): self.name = name print("Service ready") super().__init__(**kw) svc = Service("api", verbose=True) # Service ready → LogMixin ready → Base ready

**kw 透传 + 每层 super().__init__ 是多继承协作初始化的定式:每个类吃掉自己认识的参数、其余继续下传,object 收尾。谁少写了 super,链条就断在谁手里。

鸭子类型:多态的 Python 版

Java 的多态靠"实现同一接口"的类型声明;Python 只关心"运行时有没有这个方法":

class Duck: def speak(self): return "嘎嘎" class Robot: def speak(self): return "哔哔" class Person: def talk(self): return "你好" def announce(anything): return anything.speak() # 不查血统,只试方法 for actor in [Duck(), Robot()]: print(announce(actor)) # 嘎嘎 / 哔哔 announce(Person()) # AttributeError: 'Person' object has no attribute 'speak'

错误在调用点才爆——灵活与迟钝是同一枚硬币。工程上两条补丁:其一,用 hasattr/try 做能力探测(处理"可能没有"的场合);其二,用标准库的抽象基类给"有方法的约定"一个名字:

from abc import ABC, abstractmethod class Speaker(ABC): @abstractmethod def speak(self): ... class Robot(Speaker): def speak(self): return "哔哔" Robot() # 实现了 speak,可用 Speaker() # TypeError: 抽象类不能实例化

魔术方法:接入语言协议的插座

双下划线方法不是装饰,是解释器留好的插座:语法糖与内置函数在对象上的每一步,都会退到某个魔术方法。掌握它们,你的类型就能"像内置类型一样自然":

class Playlist: def __init__(self, *songs): self._songs = list(songs) def __len__(self): # len() 的插座 return len(self._songs) def __getitem__(self, i): # 下标与切片的插座,还顺带获得迭代能力 return self._songs[i] def __contains__(self, song): # in 运算符的插座 return song in self._songs def __repr__(self): # REPL 与 print 的显示插座 return f"Playlist{tuple(self._songs)!r}" pl = Playlist("晴天", "稻香", "夜曲") len(pl), pl[0], "夜曲" in pl # (3, '晴天', True) for s in pl: ... # 没写 __iter__ 也能迭代——3.1 节迭代协议的续篇

__getitem__ 免费送迭代能力是迭代协议的美妙推论:旧式协议规定"按下标从 0 取,直到 IndexError"。而实现 __iter__ + __next__ 才是现代做法(第 7.1 节细讲)。

两个必须配套的插座——__eq____hash__(呼应 2.2 节的键规则):

class Point: def __init__(self, x, y): self.x, self.y = x, y def __eq__(self, other): return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y)) # 相等对象哈希必须相等 p = Point(1, 2) seen = {Point(1, 2): "origin", p: "again"} # 值相等的键合并 len(seen) # 1

只写 __eq__ 不写 __hash__,实例立刻不可哈希、进不了 set 和 dict 键——解释器替你把一致性兜住了。常用插座速查:__str__/__repr__(显示)、__add__ 等(运算符)、__call__(实例可调用)、__enter__/__exit__(with 协议,第 6 章)、__iter__(迭代协议,第 7 章)。

继承还是组合

继承的正当用途只有两个:真正的"是一个"关系复用接口/框架扩展点(如异常体系、ABC)。为了"偷几行代码"而继承,会把两个本无关系的类焊死,父类一改全家族震动。优先组合:

class Timestamps: # 能力类:Mixin created_at = None def touch(self): import time self.created_at = time.time() class Article: # 组合/混入,而非继承某个"文章基类" def __init__(self, title): self.title = title self.ts = Timestamps() a = Article("标题"); a.ts.touch()

⚠️ 常见坑:多继承里两分支各自 super 可能重复执行某个祖先(C3 已处理菱形去重,但漏写 super 会断链);在 __eq__ 里用 == 递归比较自己;重写 __str__ 忘了 __repr__(调试时看到一堆没信息量的对象显示)。

💡 关键直觉:不要问"它是不是 Dog",问"它会不会 speak"。设计类时先设计魔术方法与协议,属性只是协议背后的存储。

本节要点回顾

  • MRO:C3 线性化给出确定查找序,__mro__ 可查、病态图定义期报错。
  • super 是沿 MRO 的接力**kw 透传 + 逐层 super().__init__ 是协作初始化定式。
  • 鸭子类型:能力而非血统决定多态;要静态约束用 ABC。
  • 魔术方法是插座__len____getitem____contains____repr__ 让自定义类型融入语言。
  • __eq____hash__ 配套:解释器强制一致性,值语义对象两者都写。
  • 组合优先:继承只留给"是一个"与框架扩展点。

第 5 章把镜头拉到比类更大的组织单位——模块与包,以及为什么"模块即命名空间"意味着属性查找故事还有上半场。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U