- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
12.4 处理多重继承
……我们需要一种更好的、全新的继承理论(目前仍是如此)。例如,继承和实例化(一种继承方式)混淆了语用(比如为了节省空间而重构代码)和语义(用途太多了,比如特殊化、普遍化、形态,等等)。
——Alan Kay
“The Early History of Smalltalk”
如 Alan Kay 所言,继承有很多用途,而多重继承增加了可选方案和复杂度。使用多重继承容易得出令人费解和脆弱的设计。我们还没有完整的理论,下面是避免把类图搅乱的一些建议。
01. 把接口继承和实现继承区分开
使用多重继承时,一定要明确一开始为什么创建子类。主要原因可能有:
继承接口,创建子类型,实现“是什么”关系
继承实现,通过重用避免代码重复
其实这两条经常同时出现,不过只要可能,一定要明确意图。通过继承重用代码是实现细节,通常可以换用组合和委托模式。而接口继承则是框架的支柱。
02. 使用抽象基类显式表示接口
现代的 Python 中,如果类的作用是定义接口,应该明确把它定义为抽象基类。Python 3.4 及以上的版本中,我们要创建 abc.ABC 或其他抽象基类的子类(如果想支持较旧的 Python 版本,参见 11.7.1 节)。
03. 通过混入重用代码
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class)。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法。
04. 在名称中明确指明混入
因为在 Python 中没有把类声明为混入的正规方式,所以强烈推荐在名称中加入 ...Mixin 后缀。Tkinter 没有采纳这个建议,如果采纳的话,XView 会变成 XViewMixin,Pack 会变成 PackMixin,图 12-3 中所有使用 «mixin» 标记的类都应该这么做。
05. 抽象基类可以作为混入,反过来则不成立
抽象基类可以实现具体方法,因此也可以作为混入使用。不过,抽象基类会定义类型,而混入做不到。此外,抽象基类可以作为其他类的唯一基类,而混入决不能作为唯一的超类,除非继承另一个更具体的混入——真实的代码很少这样做。
抽象基类有个局限是混入没有的:抽象基类中实现的具体方法只能与抽象基类及其超类中的方法协作。这表明,抽象基类中的具体方法只是一种便利措施,因为这些方法所做的一切,用户调用抽象基类中的其他方法也能做到。
06. 不要子类化多个具体类
具体类可以没有,或最多只有一个具体超类。6 也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。例如,在下述代码中,如果 Alpha 是具体类,那么 Beta 和 Gamma 必须是抽象基类或混入:
class MyConcreteClass(Alpha, Beta, Gamma): """这是一个具体类,可以实例化。""" # ……更多代码……
07. 为用户提供聚合类
如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们结合起来。Grady Booch 把这种类称为聚合类(aggregate class)。7
例如,下面是 tkinter.Widget 类的完整代码:
class Widget(BaseWidget, Pack, Place, Grid): """Internal class. Base class for a widget which can be positioned with the geometry managers Pack, Place or Grid.""" pass
Widget 类的定义体是空的,但是这个类提供了有用的服务:把四个超类结合在一起,这样需要创建新小组件的用户无需记住全部混入,也不用担心声明 class 语句时有没有遵守特定的顺序。Django 中的 ListView 类是更好的例子,稍后在 12.5 节讨论。
08. “优先使用对象组合,而不是类继承”
这句话引自《设计模式:可复用面向对象软件的基础》一书,8 这是我能提供的最佳建议。熟悉继承之后,就太容易过度使用它了。出于对秩序的诉求,我们喜欢按整洁的层次结构放置物品,程序员更是乐此不疲。
然而,优先使用组合能让设计更灵活。例如,对 tkinter.Widget 类来说,它可以不从全部几何管理器中继承方法,而是在小组件实例中维护一个几何管理器引用,然后通过它调用方法。毕竟,小组件“不是”几何管理器,但是可以通过委托使用相关的服务。这样,我们可以放心添加新的几何管理器,不必担心会触动小组件类的层次结构,也不必担心名称冲突。即便是单继承,这个原则也能提升灵活性,因为子类化是一种紧耦合,而且较高的继承树容易倒。
组合和委托可以代替混入,把行为提供给不同的类,但是不能取代接口继承去定义类型层次结构。
6在“水禽和抽象基类”中,Alex Martelli 提到 Scott Meyer 写的 More Effective C++ 一书,他做得更绝,“将非尾端类设计为抽象类”(即,具体类根本不应该有具体超类)。
7“如果一个类的结构主要继承自混入,自身没有添加结构或行为,那么这样的类称为聚合类。”Grady Booch et al., Object Oriented Analysis and Design, 3E (Addison-Wesley, 2007), p. 109.
8《设计模式:可复用面向对象软件的基础》第 13 页。
接下来,我们将从这些建议入手分析 Tkinter。
Tkinter好的、不好的和令人厌恶的方面
记住一点,自 1994 年发布的 Python 1.1 起,Tkinter 就在标准库中了。Tkinter 的底层是 Tcl 语言优秀的 GUI 工具包 Tk。Tcl/Tk 组合原本不是面向对象的,因此 Tk API 基本上就是一堆函数。尽管没有使用面向对象方式实现,但是这个工具包的理念极具面向对象思想。
前几节给出的建议 Tkinter 大都没有采用,不过第 7 点是个例外。但是 Tkinter 做得并不好,因为使用组合模式把几何管理器集成到 Widget 中更好,如第 8 点所述。
tkinter.Widget 类的文档字符串开头说它是“内部类”。这或许表明 Widget 应该定义为抽象基类。Widget 自身虽然没有方法,但是它定义了接口。它传达的意思是:“每个 Tkinter 小组件都会提供基本的方法(__init__、destroy,以及众多 Tk API 函数),此外还会提供三个几何管理器中的全部方法。”你可以不同意这是定义接口的好方式(太宽泛了),但是这样确实能定义接口,Widget 就把接口“定义”为超类接口的联合。
封装 GUI 应用逻辑的 Tk 类继承自 Wm 和 Misc,这两个类既不是抽象类,也不是混入(Wm 不算是混入,因为 TopLevel 的超类只有它一个)。Misc 类的名称本身明显是代码异味。 Misc 有 100 多个方法,而且所有小组件类都继承它。为什么每个小组件都要处理剪切板、文本选择和计时器等?我们可能不能把文本粘贴到按钮上,也不能选择滚动条里的文字。 Misc 应该拆分成几个专门的混入类,而且不是所有小组件都应该继承这些混入。
说实在的,作为 Tkinter 的用户,你根本不用知道或使用多重继承。那些都是隐藏起来的实现细节,你在自己的代码中只需实例化或子类化小组件类。不过,如果你想查找自己需要的方法,在控制台中输入 dir(tkinter.Button),你会发现列出了 214 个属性,9 此时你就是多重继承的受害者。
9目前的版本中只有 209 个属性。——编者注
除了这些问题,Tkinter 还是稳定而灵活的,未必那么不堪。陈旧(和默认)的 Tk 小组件没有考虑现代的用户界面,但是 Python 3.1(2009 年发布)提供了 tkinter.ttk 包,这个包提供的小组件很精美,外观同原生的一样,开发出的 GUI 应用也更专业。此外,有些陈旧的小组件,如 Canvas 和 Text,功能异常强大。只需少量代码,就能把一个 Canvas 对象打造成简单的拖拽绘图应用。如果你对 GUI 编程感兴趣,Tkinter 和 Tcl/Tk 绝对值得一看。
然而,我们的主题不是 GUI 编程,而是多重继承的运用。显式使用混入类的现代示例在 Django 中可以找到。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论