4.6 多态和封装
“继承”是类的一个重要特征,在编程中用途很多,虽然在某些具体、细节的层面还有一些不同的看法,但是,这里要说的“多态”和“封装”无论是在理解上还是在实践上都是有争议的话题。所谓争议,多来自于对同一个现象不同角度的理解,特别是有不少经验丰富的程序员,还从其他语言的角度来诠释Python的多态等。不管有多少不同的理解方式,我们都要对这两个东西有所了解,因为它们是你编程水平进阶的必需。
4.6.1 多态
到网上搜索“多态”,仁者见仁智者见智。Python中关于多态的基本体现,可以通过下面的方式来理解。
- >>> "This is a book".count("s")
- 2
- >>> [1,2,4,3,5,3].count(3)
- 2
count()函数的作用是数一数某个元素在对象中出现的次数。从例子中可以看出,我们并没有限定count的参数所引入的值应该是什么类型的。类似的例子还有:
- >>> f = lambda x, y: x + y
还记得这个lambda函数吗?
- >>> f(2, 3)
- 5
- >>> f("qiw", "sir")
- 'qiwsir'
- >>> f(["python", "java"], ["c++", "lisp"])
- ['python', 'java', 'c++', 'lisp']
在这个lambda函数中,我们没有限制应该传入什么类型的对象(或者说数据、值),也一定不能限制,因为如果限制了,就不是pythonic了。也就是说,允许给参数传任意类型的数据,并返回相应的结果,至于是否报错,则取决于“+”的能力范围。这就是“多态”的表现。
“多态”是否能正确表达,不是通过限制传入的对象类型实现,而是这样处理:
- >>> f("qiw", 2)
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "<stdin>", line 1, in <lambda>
- TypeError: cannot concatenate 'str' and 'int' objects
这个例子中,把判断两个对象是否能够相加的任务交给了“+”,不是放在入口处判断类型是否为字符串或者数字。
申明,本书由于无意对概念进行讨论,所以不进行这方面的深入探索,仅仅是告诉各位读者相关信息。并且,既然大多数程序员都在讨论多态,那么我们就按照大多数人说的去介绍。
“多态”(Polymorphism),维基百科中对此有详细解释说明。
多型(英语:Polymorphism),是指面向对象程序执行时,相同的信息可能会送给多个不同的类别对象,系统可依据对象所属类别,引发对应类别的方法而有不同的行为。简单来说,所谓多型意指相同的信息给予不同的对象会引发不同的动作。
简化的说法就是“有多种形式”,就算不知道变量(参数)所引用的对象类型,也一样能进行操作,来者不拒,比如上面显示的例子。在Python中,更为pythonic的做法是根本就不进行类型检验。
例如著名的repr()函数,它能够针对输入的任何对象返回一个字符串,这就是多态的代表之一。
- >>> repr([1, 2, 3])
- '[1, 2, 3]'
- >>> repr(1)
- '1'
- >>> repr({"lang": "python"})
- "{'lang': 'python'}"
使用它写一个小函数,还是作为多态举例。
- >>> def length(x):
- ... print "The length of", repr(x), "is", len(x)
- ...
- >>> length("how are you")
- The length of 'how are you' is 11
- >>> length([1, 2, 3])
- The length of [1, 2, 3] is 3
- >>> length({"lang":"python","book":"itdiffer.com"})
- The length of {'lang': 'python', 'book': 'itdiffer.com'} is 2
不过,多态也不是万能的,如果这样做:
- >>> length(7)
- The length of 7 is
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "<stdin>", line 2, in length
- TypeError: object of type 'int' has no len()
报错了。看错误提示,明确告诉了我们“object of type'int'has no len()”,也就是说,函数length()中的len()会对传入的对象进行检验,如果不符合要求,就会报错,使用者可以根据报错信息对传入的对象类型进行调整。
在诸多介绍多态的文章中都会有关于“猫和狗”的例子。这里也将代码贴出来,读者去体会所谓多态体现。其实,如果你进入了Python的语境,有时不经意间就在应用多态特性。
- #!/usr/bin/env python
- # coding=utf-8
- '''
- the code is from: http://zetcode.com/lang/python/oop/
- '''
- __metaclass__ = type
- class Animal:
- def __init__(self, name = ""):
- self.name = name
- def talk(self):
- pass
- class Cat(Animal):
- def talk(self):
- print "Meow!"
- class Dog(Animal):
- def talk(self):
- print "Woof!"
- a = Animal()
- a.talk()
- c = Cat("Missy")
- c.talk()
- d = Dog("Rocky")
- d.talk()
保存后运行之:
- $ python 21101.py
- Meow!
- Woof!
代码中有Cat和Dog两个类,都继承了类Animal,它们都有talk()方法,输入不同的动物名称,会得出相应的结果。
关于多态,有一个被称作“鸭子类型”(duck typeing)的东西,其含义在维基百科中被表述为:
在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。这个概念的名字来源于James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
最后要提示读者,类型检查是毁掉多态的利器,比如type、isinstance以及isubclass函数,所以,一定要慎用这些类型检查函数。
4.6.2 封装和私有化
在正式介绍封装之前,先讲个笑话。
某软件公司老板号称自己懂技术。一次有一个项目要交付给客户,他不想让客户知道实现某些功能的代码,但是交付的时候必须要给人家代码。于是该老板就告诉程序员,“你们把那部分核心代码封装一下”。程序员听完迷茫了。
很多人没有笑,因为不明白说的是什么,不知道你有没有笑。这种幽默唯一的价值在于提到了一个词语“封装”。
“封装”是不是把代码写到某个东西里面,“人”在编辑器中打开也看不到呢?不是,除非你的显示器坏了。
在程序设计中,封装(Encapsulation)是对对象(object)的一种抽象,即将某些部分隐藏起来,在程序外部看不到,无法调用(不是人用眼睛看不到那个代码,除非用某种加密或者混淆方法,造成显示的是一堆混乱的代码,但这不是封装)。
要了解封装离不开“私有化”,就是将类或者函数中的某些属性限制在某个区域之内,外部无法调用,所以先说“私有化”。
“私有化”,顾名思义,就是将某个对象(这个对象可以是你认为的东西)限制在某个自己认定的范围内。比如,某国经济实行私有化,就是将经济体系中组成部分分别纳入到个人权限范畴,而不是放在众多个人无法触及的领域(那是公有),或者说,私有化就是产权清晰。与之对应的是“公有”。
Python中私有化的方法也比较简单,就是在准备私有化的属性(包括方法、数据)名字前面加双下画线。例如:
- #!/usr/bin/env python
- # coding=utf-8
- __metaclass__ = type
- class ProtectMe:
- def __init__(self):
- self.me = "qiwsir"
- self.__name = "kivi"
- def __python(self):
- print "I love Python."
- def code(self):
- print "Which language do you like?"
- self.__python()
- if __name__ == "__main__":
- p = ProtectMe()
- print p.me
- print p.__name
运行一下,看看效果:
- $ python 21102.py
- qiwsir
- Traceback (most recent call last):
- File "21102.py", line 21, in <module>
- print p.__name
- AttributeError: 'ProtectMe' object has no attribute '__name'
查看报错信息,告诉我们没有__name
那个属性。果然隐藏了,在类的外面无法调用。再试试类里面的那个code()是否可以使用?把该程序做适当修改。
- if __name__ == "__main__":
- p = ProtectMe()
- p.code()
- p.__python()
修改好之后保存。其中p.code()的意图是要打印出两句话:“Which language do you like?”和“I love Python.”,code()方法和私有化的python()方法在同一个类中,按照私有化的含义,在类里面应该是可以调用的。而p.python()试图通过实例在类的外面调用它。看看效果:
- $ python 21102.py
- Which language do you like?
- I love Python.
- Traceback (most recent call last):
- File "21102.py", line 23, in <module>
- p.__python()
- AttributeError: 'ProtectMe' object has no attribute '__python'
如愿以偿,该调用的调用了,该隐藏的隐藏了。
用上面的方法的确做到了封装。但是,如果要调用那些私有属性怎么办?
可以使用property函数。请看下面的例子:
- #!/usr/bin/env python
- # coding=utf-8
- __metaclass__ = type
- class ProtectMe:
- def __init__(self):
- self.me = "qiwsir"
- self.__name = "kivi"
- @property
- def name(self):
- return self.__name
- if __name__ == "__main__":
- p = ProtectMe()
- print p.name
运行结果:
- $ python 21102.py
- kivi
从上面可以看出,用了@property之后,再调用那个方法的时候,用p.name的形式,就好像在调用以往非私有化属性一样。
看来,封装的确不是“让人看不见”。