>>> from env_helper import info; info()
页面更新时间: 2023-06-22 15:12:42
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-9-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

9.1. 面向对象的概念

在面向对象编程中,术语“对象”大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法。 使用对象而非全局变量和函数的原因有多个,下面列出了使用对象的最重要的好处。

  • 多态:可对不同类型的对象执行相同的操作。

  • 封装:对外部隐藏有关对象工作原理的细节。

  • 继承:可基于通用类创建出专用类。

9.1.1. 多态的概念

术语多态(polymorphism)源自希腊语,意思是“有多种形态”。 这意味着即便你不知道变量指向的是哪种对象,也能够对其执行操作,且操作的行为将随对象所属的类型(类)而异。 例如,假设你要为一个销售食品的电子商务网站创建在线支付系统,程序将接收来自系统另一部分(或之后设计的类似系统)的购物车。 因此你只需计算总价并从信用卡扣除费用即可。

首先,指定程序收到商品时必须如何表示。例如,你可能要求用元组表示收到的商品:

>>> ('SPAM', 2.50)
('SPAM', 2.5)

这样表示标签和价格很好,但不太灵活。假设该网站新增了拍卖服务,即不断降低商品的价格,直到有人购买为止。 在这种情况下,如果能够允许用户像下面这样做就好了:将商品放入购物车并进入结算页面,等到价格合适时再单击“支付”按钮。

然而,使用简单的元组表示商品无法做到这一点。 要做到这一点,表示商品的对象必须在你编写的代码询问价格时通过网络检查其当前价格,也就是说不能像在元组中那样固定价格。那么该如何做呢?

让对象自己去处理这种操作。 这好像没什么大不了,但仔细想想将发现,这样事情将简单得多:每种新对象都能够获取或计算其价格并返回结果,而你只需向它们询问价格即可。 这正是多态的用武之地。

9.1.2. 多态和方法

你收到一个对象,却根本不知道它是如何实现的——它可能是众多“形态”中的任何一种。 你只知道可以询问其价格,但这就够了。至于询问价格的方式,你应该很熟悉。

object.get_price()

像这样与对象属性相关联的函数称为方法。你在前面见过这样的函数:字符串、列表和 字典的方法。多态你其实也见过。

>>> 'abc'.count('a')
1
>>> [1, 2, 'a'].count('a')
1

如果有一个变量 x ,你无需知道它是字符串还是列表就能调用方法 count() : 只要你向这个方法 提供一个字符作为参数,它就能正常运行。

下面来做个实验。标准库模块 random 包含一个名为 choice() 的函数,它从序列中随机选择一个元素。 下面使用这个函数给变量提供一个值。

>>> from random import choice
>>> x = choice(['Hello, world!', [1, 2, 'e', 'e', 4]])

执行这些代码后,x可能包含字符串 'Hello, world!' ,也可能包含列表 [1, 2, 'e', 'e', 4] 。 具体是哪一个,你不知道也不关心。 你只关心 x 包含多少个 'e' ,而不管 x 是字符串还是列表你都 能找到答案。 为找到答案,可像前面那样调用 count()

>>> x.count('e')
2

上面的结果,可能是 1 也可能是 2 。 但关键在于你无需执行相关的检查,只要 x 有一个名为 count() 的方法,它将单个字符作为参数并返回一个整数就行。 如果有人创建了包含这个方法的对象,你也可以像使用字符串和列表一样使用这种对象。

多态形式多样

每当无需知道对象是什么样的就能对其执行操作时,都是多态在起作用。这不仅仅适用于方 法,我们还通过内置运算符和函数大量使用了多态。请看下面的代码:

>>> 1 + 2
3

3

>>> 'Fish' + 'license' 'Fishlicense'
'FishlicenseFishlicense'

上述代码表明,加法运算符 + 既可用于数(这里是整数),也可用于字符串(以及其他类型的序列)。 为证明这一点,假设你要创建一个将两个对象相加的 add 函数, 可像下面这样定义它 (这与模块 operator 中的函数 add 等价,但效率更低):

>>> def add(x, y):
>>>     return x + y

可使用众多不同类型的参数来调用这个函数。

>>> add(1, 2)
3
>>> add('Fish', 'license')
'Fishlicense'

这也许有点傻,但重点在于参数可以是任何支持加法的对象。 如果要编写一个函数,通过打印一条消息来指出对象的长度,可以像下面这样做(它对参数的唯一要求是有长度,可对其执行函数 len() )。

>>> def length_message(x):
>>>     print("The length of", repr(x), "is", len(x))

如你所见,这个函数还使用了 reprrepr是多态的集大成者之一,可用于任何对象,下面就来看看:

>>> length_message('Fnord')
The length of 'Fnord' is 5
>>> length_message([1, 2, 3])
The length of [1, 2, 3] is 3

很多函数和运算符都是多态的,你编写的大多数函数也可能如此,即便你不是有意为之。 每当你使用多态的函数和运算符时,多态都将发挥作用。 事实上,要破坏多态,唯一的办法是使用诸如 typeissubclass 等函数显式地执行类型检查,但你应尽可能避免以这种方式破坏多态。 重要的是,对象按你希望的那样行事,而非它是否是正确的类型(类)。然而,不要使用类型检查的禁令已不像以前那么严格。

注意 这里讨论的多态形式是Python编程方式的核心,有时称为鸭子类型。这个术语源自如下说法:“如果走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”

9.1.3. 封装

封装(encapsulation)指的是向外部隐藏不必要的细节。这听起来有点像多态。 这两个概念很像,因为它们都是抽象的原则。它们都像函数一样, 可帮助你处理程序的组成部分,让你无需关心不必要的细节。

但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封 装让你无需知道对象的构造就能使用它。 听起来还是有点像?下面来看一个使用了多态但没有使用封装的示例。 假设你有一个名为 OpenObject 的类。

>>> class OpenObject:
>>>     def __init__(self):
>>>         self.name = ''
>>>     def set_name(self, para):
>>>         print(para)
>>>         self.name = para
>>>     def get_name(self):
>>>         print(self.name)

对象就是这样创建的

>>> o = OpenObject()
>>> o.set_name('Sir Hello')
Sir Hello
>>> o.get_name()
Sir Hello

你(通过像调用函数一样调用类)创建一个对象,并将其关联到变量 o ,然后就可以使用方法 set_nameget_name了(假设OpenObject支持这些方法)。 一切都看起来完美无缺。然而,如果 o 将其名称存储在全局变量 global_name 中呢?

global_name

这意味着使用OpenObject类的实例(对象)时,你需要考虑global_name的内容。事实上,必须确保无人能修改它。

>>> global_name = 'Sir Gumby'
>>> o.get_name()

如果尝试创建多个OpenObject对象,将出现问题,因为它们共用同一个变量。

o1 = OpenObject()
o2 = OpenObject()
o1.set_name('Robin Hood')
o2.get_name()

如你所见,设置一个对象的名称时,将自动设置另一个对象的名称。这可不是你想要的结果。

基本上,你希望对象是抽象的:当调用方法时,无需操心其他的事情,如避免干扰全局变量。 如何将名称“封装”在对象中呢?没问题,将其作为一个属性即可。

属性是归属于对象的变量,就像方法一样。实际上,方法差不多就是与函数相关联的属性 。如果你使用属性而非全局变量重新编写前面的 类,并将其重命名为ClosedObject,就可像下面这样使用它:

c = ClosedObject()
c.set_name('Sir Lancelot')
c.get_name()

到目前为止一切顺利,但这并不能证明名称不是存储在全局变量中的。下面再来创建一个 对象。

>>> r = ClosedObject()
>>> r.set_name('Sir Robin')
r.get_name()
'Sir Robin'

从中可知正确地设置了新对象的名称,但第一个对象现在怎么样了呢?

>>> c.get_name()
'Sir Lancelot'

其名称还在!因为这个对象有自己的状态。对象的状态由其属性(如名称)描述。对象的方 法可能修改这些属性,因此对象将一系列函数(方法)组合起来,并赋予它们访问一些变量(属 性)的权限,而属性可用于在两次函数调用之间存储值。

9.1.4. 继承

继承是另一种偷懒的方式(这里是褒义)。程序员总是想避免多次输入同样的代码,继承可以在一定程度上减少重复的代码。 本教程前面通过创建函数来达成这个目标,但现在要解决一个更微妙的问题。 如果你已经有了一个类,并要创建一个与之很像的类,该如何办呢? 创建这个新类时,你不想 复制旧类的代码,将其粘贴到新类中。

例如,你可能已经有了一个名为Shape的类,它知道如何将自己绘制到屏幕上。现在你想创 建一个名为Rectangle的类,但它不仅知道如何将自己绘制到屏幕上,而且还知道如何计算其面 积。你不想重新编写方法draw,因为Shape已经有一个这样的方法,且效果很好。那么该如何办 呢?让Rectangle继承Shape的方法,使得对Rectangle对象调用方法draw时,将自动调用Shape类的这个方法。