1、对象魔法
在面向对象编程中,术语对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法。使用对象而非全局变量和函数的原因有多个,下面列出了使用对象的最重要的好处
多态:可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够正常运行
封装:对外部隐藏有关对象工作原理的细节。
继承:可基于通用类创建出专用类
1.1 多态
术语多态(polymorphism)源自希腊语,意思是“有多种形态”。这大致意味着即便你不知道变量指向的是哪种对象,也能够对其执行操作,且操作的行为将随对象所属的类型(类)而异
1.2 多态和方法
对象属性相关联的函数称为方法。
1.3 封装
封装(encapsulation)指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对象的内部细节就可使用它)。这两个概念很像,因为它们都是抽象的原则。它们都像函数一样,可帮助你处理程序的组成部分,让你无需关心不必要的细节
但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造就能使用它
2 类
2.1 类到底是什么
每个对象都属于特定的类,并被称为该类的实例。
2.2 创建自定义类
这个示例包含三个方法定义,它们类似于函数定义,但位于class语句内。std当然是类的名称。class语句创建独立的命名空间,用于在其中定义函数
2.3 属性、函数和方法
实际上,方法和函数的区别表现在前一节提到的参数self上
2.4 再谈隐藏
Python没有为私有属性提供直接的支持,而是要求程序员知道在什么情况下从外部修改属性是安全的。毕竟,你必须在知道如何使用对象之后才能使用它。然而,通过玩点小花招,可获得类似于私有属性的效果
要让方法或属性成为私有的(不能从外部访问),只需让其名称以两个下划线打头即可
虽然以两个下划线打头有点怪异,但这样的方法类似于其他语言中的标准私有方法。然而,幕后的处理手法并不标准:在类定义中,对所有以两个下划线打头的 名称都进行转换,即在开头加上一个下划线和类名
总之,你无法禁止别人访问对象的私有方法和属性,但这种名称修改方式发出了强烈的信号,让他们不要这样做
2.5 类的命名空间
它们都创建一个返回参数平方的函数,并将这个函数关联到变量foo。可以在全局(模块)作用域内定义名称foo,也可以在函数或方法内定义。定义类时情况亦如此:在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段,并非所有的Python程序员都知道这一点,但知道这一点很有帮助。
每个实例都可访问这个类作用域内的变量,就像方法一样。
注意:类有自己的变量,该变量是所有对象共享,通过类名加点加变量名访问修改;而每个类对象也有自己的变量,该变量每个对象都不一样,通过对象名加点加变量名修改访问
2.6 指定超类
超类又名父类
子类扩展了超类的定义。要指定超类,可在class语句中的类名后加上超类名,并将其用圆括号括起
2.7 深入探讨继承
要确定一个类是否是另一个类的子类,可使用内置方法issubclass。
如果你有一个类,并想知道它的基类,可访问其特殊属性__bases__
同样,要确定对象是否是特定类的实例,可使用isinstance。
如果你要获悉对象属于哪个类,可使用属性__class__。
2.8 多个超类
子类TalkingCalculator本身无所作为,其所有的行为都是从超类那里继承的。关键是通过从Calculator那里继承calculate,并从Talker那里继承talk,它成了会说话的计算器
这被称为多重继承,是一个功能强大的工具。然而,除非万不得已,否则应避免使用多重继承,因为在有些情况下,它可能带来意外的“并发症”
使用多重继承时,有一点务必注意:如果多个超类以不同的方式实现了同一个方法(即有多个同名方法),必须在class语句中小心排列这些超类,因为位于前面的类的方法将覆盖位于后面的类的方法。因此,在前面的示例中,如果Calculator类包含方法talk,那么这个方法将覆盖Talker类的方法talk(导致它不可访问)。如果像下面这样反转超类的排列顺序:
class TalkingCalculator(Talker, Calculator): pass
将导致Talker的方法talk是可以访问的。多个超类的超类相同时,查找特定方法或属性时访问超类的顺序称为方法解析顺序(MRO),它使用的算法非常复杂。所幸其效果很好,你可能根本无需担心
2.9 接口和内省
接口这一概念与多态相关。处理多态对象时,你只关心其接口(协议)——对外暴露的方法和属性。在Python中,不显式地指定对象必须包含哪些方法才能用作参数。例如,你不会像在Java中那样显式编写接口,而是假定对象能够完成你要求它完成的任务。如果不能完成,程序将失败
通常,你要求对象遵循特定的接口(即实现特定的方法),但如果需要,也可非常灵活地提出要求:不是直接调用方法并期待一切顺利,而是检查所需的方法是否存在;如果不存在,就改弦易辙
>>> hasattr(tc, 'talk')
True
>>> hasattr(tc, 'fnord')
False
在上述代码中,你发现tc(本章前面介绍的TalkingCalculator类的实例)包含属性talk(指向一个方法),但没有属性fnord
要查看对象中存储的所有值,可检查其__dict__属性
2.10 抽象基类
最终,Python通过引入模块abc提供了官方解决方案。这个模块为所谓的抽象基类提供了支持。一般而言,抽象类是不能(至少是不应该)实例化的类,其职责是定义子类应实现的一组抽象方法。
抽象类(即包含抽象方法的类)最重要的特征是不能实例化
形如@this的东西被称为装饰器,其用法将在第9章详细介绍。这里的要点是你使用@abstractmethod来将方法标记为抽象的——在子类中必须实现的方法
3 小结
对象:对象由属性和方法组成。属性不过是属于对象的变量,而方法是存储在属性中的函数。相比于其他函数,(关联的)方法有一个不同之处,那就是它总是将其所属的对象作为第一个参数,而这个参数通常被命名为self
类:类表示一组(或一类)对象,而每个对象都属于特定的类。类的主要任务是定义其实例将包含的方法
多态:多态指的是能够同样地对待不同类型和类的对象,即无需知道对象属于哪个类就可调用其方法
封装:对象可能隐藏(封装)其内部状态。在有些语言中,这意味着对象的状态(属性)只能通过其方法来访问。在Python中,所有的属性都是公有的,但直接访问对象的状态时程序员应谨慎行事,因为这可能在不经意间导致状态不一致。
继承:一个类可以是一个或多个类的子类,在这种情况下,子类将继承超类的所有方法。你可指定多个超类,通过这样做可组合正交(独立且不相关)的功能。为此,一种常见的做法是使用一个核心超类以及一个或多个混合超类。
接口和内省:一般而言,你无需过于深入地研究对象,而只依赖于多态来调用所需的方法。然而,如果要确定对象包含哪些方法或属性,有一些函数可供你用来完成这种工作。
抽象基类:使用模块abc可创建抽象基类。抽象基类用于指定子类必须提供哪些功能,却不实现这些功能
面向对象设计:关于该如何进行面向对象设计以及是否该采用面向对象设计,有很多不同的观点。无论你持什么样的观点,都必须深入 理解问题,进而创建出易于理解的设计