多态和封装
一、多态
术语多态(polymorphism)源自希腊语,意思是“有多种形态”。这大致意味着即便你不知道变量指向的哪种对象,
也能够对其执行操作,且操作的行为将随所属的类型而异。
例如,假设你要为一个销售食品的电子商务网站创建在线支付系统,程序将接受来自系统另一部分的购物车。因此你只需要计算总价并从信用卡中扣除费用即可。
你首先想到的可能是,指定程序收到商品时必须如何表示。例如,呢可能要求用元组表示收到的商品。如下所示:
('apple',4)
如果你只需要描述性标签和价格,这样的表示很好,但不灵活。假设该网站新增了拍卖服务,即不断降低商品的价格,直到有人购买为止。
在这种情况下,如果能够允许用户像下面这样做就好了:将商品放入购物车并进入结算页面,等到价格合适时再点击支付。
然而,使用简单的元组表示商品无法做到这一点。要做到这一点,表示商品的对象必须在你编写的代码询问价格时通过网络检查其当前价格,也就是说不能像在元组中那样固定价格。要解决这个问题,可创建一个函数。
def get_price(object): if isinstance(object,tuple): return object[1] else: return magic_network(object)
# 这里使用isinstance 来执行类型检查旨在说明:使用类型价差通常是馊主意,应尽可能避免。
前面的代码使用函数isinstance来检查object是否是元组。如果是,就返回其第二个元素,否则就调用一个神奇的网络方法。
如果网络方法已就绪,问题就暂时解决了。但这种解决方案还是不太灵活。如果有位程序员很聪明,决定用十六进制的字符串表示价格,并将其存储在字典的‘price'键下呢?没问题,你只需要更新相应的函数。
def get_price(object): if isinstance(object,tuple): return object[1] elif isinstance(object,dict): return int(object['price']) else: return magic_network(object)
你确定现在考虑到了多样的可能性了吗?假设有人决定添加一种新字典,并在其中将价格存储在另一个键下,你该如何办呢?当然,可再次更新get_price,但这种应对之策在多长时间内有效呢? 每当有人以不同的方式实现对象时,你都需要重新实现你的模块。如果你将该模块卖给了别人,转而从事其他项目的开发,客户该怎么办?显然。这种实现不同行为的方式既不灵活也不切实际。
那么该如何做呢?让对象自己去处理这种操作。这好像没什么大不了,但仔细想想将发现,这样事情将简单得多:每种新对象都能够获取或计算并返回结果,而你只需要向它们询问价格即可。这正是多态的用武之地。
多态和方法
你收到一个对象,却根本不知道它是如何实现的----它可能是众多“形态”中任何一种。你只知道可以询问其价格,但这就够了。至于询问价格的方式,你应该熟悉。
print(object.get_price()) 4
像这样与对象属性相关的函数称为方法。
其实好多内置函数就用来多态的方式。
str1= “abcd" str1.count("a") len(str1) list1 = [1,2,3,4] len(list1)
像这样无需知道变量是字符串还是列表就能调用方法len,只要你能向这个方法提供一个字符作为参数,它就能正常运行。
多态形式多样
每当无需知道对象是什么样的就能对其执行操作时,都是多态在起作用。这不仅仅适用于方法,我们还通过内置运算符合函数大量使用了多态。
print(1 + 2) 3 price("hello" + "world") helloworld
上述代码表明,加法运算符号(+)即可用于数学运算,也可用于字符串(以及其他类型的序列)。
很多函数和运算符都是多态的,你编写的大多数函数也可能如此,即便你不是有意为之。每当你使用多态的函数和运算符时,多态都将发挥作用。
事实上,要破坏多态,唯一的方法就是使用诸如type、issubclass 等函数显式地执行类型检查,但你应尽可能避免以这种方式破坏多态。
重要的是,对象按你希望那那样行事,而非它是否是正确的类型(类)。然而,不用使用类型检查的禁令已不像以前那么严格。引入本章后面讲讨论的抽象基类和模块abc后,issubclass本身也就是多态了!
鸭子类型
多态是Python编程方式的核心,有时称为鸭子类型。这个术语源自如下说法:如果走起来像鸭子,叫起来像鸭子,那么它就是鸭子。
二 、封装
封装(encapsulation)指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对象内部的细节就可使用它)。
这两个概念很像,因为它们都是抽象的原则。它们都像函数一样,可帮助你处理程序的组成部分,让你无需关心不必要的细节。
但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造方法就能使用它。
听起来还是有点像?下面看一个使用了多态但没有使用封装的示例。下来创建一个名为OpenObject的类。
class OpenObject(): def __init__(self): pass def set_name(self,name): self.name= name def get_name(self): return self.name
实例化一个对象
o=OpenObject() o.set_name("Sir Lancelot") o.get_name() Sir Lancelot
通过像调用函数一样调用类来创建了一个对象,并将其关联到o,然后就可以使用方法set_name和get_name了。
一切看起来都完美无缺。然而,如果o将其名称存储在全局变量 global_name中呢?这意味着使用OpenObject类的实例时,你需要考虑global_name的内容,事实上,必须确保无人能修改它。
class OpenObject(): name = "Sir Lancelot" def __init__(self): pass def set_name(self, name): OpenObject.name = name def get_name(self): return self.name
如果创建多个OpenObject,将出现问题,因为它们共用一个变量
o = OpenObject() o1 = OpenObject() print(o1.get_name()) o.set_name("Sir Grub") print(o.get_name()) print(o1.get_name())
结果为:
Sir Lancelot Sir Grub Sir Grub
这说明在设置一个对象的名称时,将自动设置另一个对象的名称。这可不是想要的结果。基本上我们都希望对象时抽象的;当调用方法时,无需操心其他的操作,如避免干扰全局变量,如何将名称“封装”到对象中呢? 没问题,将其作为一个属性即可。
属性时归属于对象的变量,就像方法一样。实际上,方法差不多就是与函数相关联的属性,如果使用属性而非全局变量重新编写起那么的类,并将其重命名为CloseObject,就可以像下面这样做:
class OpenObject(): def __init__(self): pass def set_name(self, name): self.name = name def get_name(self): return self.name
o = OpenObject() o.set_name("Sir Grub") print(o.get_name()) 结果为: Sir Grub
到目前为止一切顺利,但这并不能证明名称不是存储在全局变量中的。下面再来创建一个对象。
o1 = OpenObject() print(o1.get_name()) 结果为: Sir Lancelot
从中可知正确的设置了新对象的名称,但第一个对现在怎么样了呢?
print(o.get_name()) Sir Grub
其名称还在! 因为这个对象有自己的状态,对象的属性由其属性(如名称)描述。对象的方法肯修改这些属性,因此对象将一系列函数(方法)组合起来,并赋予他们访问一些变量(属性)的权限,而属性可用在两次函数调用之间存储值。