再谈抽象
7.1 对象魔法
7.1.2 多态和方法
你收到一个对象,却根本不知道它是如何实现的——它可能是众多“形态”中的任何一种。
你只知道可以询问其价格,但这就够了。至于询问价格的方式,你应该很熟悉。
>>> object.get_price() 2.5
像这样与对象属性相关联的函数称为方法。你在本书前面见过这样的函数:字符串、列表和 字典的方法。多态你其实也见过。
>>> '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
从上述结果看,x包含的应该是列表。但关键在于你无需执行相关的检查,只要x有一个名为
count的方法,它将单个字符作为参数并返回一个整数就行。如果有人创建了包含这个方法的对
象,你也可以像使用字符串和列表一样使用这种对象。
多态形式多样
每当无需知道对象是什么样的就能对其执行操作时,都是多态在起作用。这不仅仅适用于方
1
>>> 'Fish' + 'license' 'Fishlicense'
>>> add('Fish', 'license') 'Fishlicense'
这也许有点傻,但重点在于参数可以是任何支持加法的对象①。如果要编写一个函数,通过 打印一条消息来指出对象的长度,可以像下面这样做(它对参数的唯一要求是有长度,可对其执 行函数len)。
def length_message(x):
print("The length of", repr(x), "is", len(x))
如你所见,这个函数还使用了repr。repr是多态的集大成者之一,可用于任何对象,下面就 来看看:
>>> length_message('Fnord') The length of 'Fnord' is 5
>>> length_message([1, 2, 3]) The length of [1, 2, 3] is 3
① 请注意,这些对象必须支持它们之间的加法,因此调用add(1, 'license')不可行。
7.1.3 封装
封装(encapsulation)指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对 象的内部细节就可使用它)。这两个概念很像,因为它们都是抽象的原则。它们都像函数一样,
可帮助你处理程序的组成部分,让你无需关心不必要的细节。
但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封 装让你无需知道对象的构造就能使用它。听起来还是有点像?下面来看一个使用了多态但没有使 用封装的示例。假设你有一个名为OpenObject的类(如何创建类将在本章后面介绍)。
>>> o = OpenObject() # 对象就是这样创建的
>>> o.set_name('Sir Lancelot')
>>> o.get_name() 'Sir Lancelot'
你(通过像调用函数一样调用类)创建一个对象,并将其关联到变量o,然后就可以使用方 法set_name和get_name了(假设OpenObject支持这些方法)。一切都看起来完美无缺。然而,如果 o将其名称存储在全局变量global_name中呢?
>>> global_name 'Sir Lancelot'
这意味着使用OpenObject类的实例(对象)时,你需要考虑global_name的内容。事实上,必 须确保无人能修改它。
>>> global_name = 'Sir Gumby'
>>> o.get_name() 'Sir Gumby'
如果尝试创建多个OpenObject对象,将出现问题,因为它们共用同一个变量。
>>> o1 = OpenObject()
>>> o2 = OpenObject()
>>> o1.set_name('Robin Hood')
>>> o2.get_name() 'Robin Hood'
如你所见,设置一个对象的名称时,将自动设置另一个对象的名称。这可不是你想要的结果。
基本上,你希望对象是抽象的:当调用方法时,无需操心其他的事情,如避免干扰全局变量。
如何将名称“封装”在对象中呢?没问题,将其作为一个属性即可。
属性是归属于对象的变量,就像方法一样。实际上,方法差不多就是与函数相关联的属性
(7.2.3节将介绍方法和函数之间的一个重要差别)。如果你使用属性而非全局变量重新编写前面的 类,并将其重命名为ClosedObject,就可像下面这样使用它:
>>> c = ClosedObject()
>>> c.set_name('Sir Lancelot')
>>> c.get_name() 'Sir Lancelot'
到目前为止一切顺利,但这并不能证明名称不是存储在全局变量中的。下面再来创建一个