4.6 多态和封装

“继承”是类的一个重要特征,在编程中用途很多,虽然在某些具体、细节的层面还有一些不同的看法,但是,这里要说的“多态”和“封装”无论是在理解上还是在实践上都是有争议的话题。所谓争议,多来自于对同一个现象不同角度的理解,特别是有不少经验丰富的程序员,还从其他语言的角度来诠释Python的多态等。不管有多少不同的理解方式,我们都要对这两个东西有所了解,因为它们是你编程水平进阶的必需。

4.6.1 多态

到网上搜索“多态”,仁者见仁智者见智。Python中关于多态的基本体现,可以通过下面的方式来理解。

  1. >>> "This is a book".count("s")
  2. 2
  3. >>> [1,2,4,3,5,3].count(3)
  4. 2

count()函数的作用是数一数某个元素在对象中出现的次数。从例子中可以看出,我们并没有限定count的参数所引入的值应该是什么类型的。类似的例子还有:

  1. >>> f = lambda x, y: x + y

还记得这个lambda函数吗?

  1. >>> f(2, 3)
  2. 5
  3. >>> f("qiw", "sir")
  4. 'qiwsir'
  5. >>> f(["python", "java"], ["c++", "lisp"])
  6. ['python', 'java', 'c++', 'lisp']

在这个lambda函数中,我们没有限制应该传入什么类型的对象(或者说数据、值),也一定不能限制,因为如果限制了,就不是pythonic了。也就是说,允许给参数传任意类型的数据,并返回相应的结果,至于是否报错,则取决于“+”的能力范围。这就是“多态”的表现。

“多态”是否能正确表达,不是通过限制传入的对象类型实现,而是这样处理:

  1. >>> f("qiw", 2)
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. File "<stdin>", line 1, in <lambda>
  5. TypeError: cannot concatenate 'str' and 'int' objects

这个例子中,把判断两个对象是否能够相加的任务交给了“+”,不是放在入口处判断类型是否为字符串或者数字。

申明,本书由于无意对概念进行讨论,所以不进行这方面的深入探索,仅仅是告诉各位读者相关信息。并且,既然大多数程序员都在讨论多态,那么我们就按照大多数人说的去介绍。

“多态”(Polymorphism),维基百科中对此有详细解释说明。

多型(英语:Polymorphism),是指面向对象程序执行时,相同的信息可能会送给多个不同的类别对象,系统可依据对象所属类别,引发对应类别的方法而有不同的行为。简单来说,所谓多型意指相同的信息给予不同的对象会引发不同的动作。

简化的说法就是“有多种形式”,就算不知道变量(参数)所引用的对象类型,也一样能进行操作,来者不拒,比如上面显示的例子。在Python中,更为pythonic的做法是根本就不进行类型检验。

例如著名的repr()函数,它能够针对输入的任何对象返回一个字符串,这就是多态的代表之一。

  1. >>> repr([1, 2, 3])
  2. '[1, 2, 3]'
  3. >>> repr(1)
  4. '1'
  5. >>> repr({"lang": "python"})
  6. "{'lang': 'python'}"

使用它写一个小函数,还是作为多态举例。

  1. >>> def length(x):
  2. ... print "The length of", repr(x), "is", len(x)
  3. ...
  4. >>> length("how are you")
  5. The length of 'how are you' is 11
  6. >>> length([1, 2, 3])
  7. The length of [1, 2, 3] is 3
  8. >>> length({"lang":"python","book":"itdiffer.com"})
  9. The length of {'lang': 'python', 'book': 'itdiffer.com'} is 2

不过,多态也不是万能的,如果这样做:

  1. >>> length(7)
  2. The length of 7 is
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. File "<stdin>", line 2, in length
  6. TypeError: object of type 'int' has no len()

报错了。看错误提示,明确告诉了我们“object of type'int'has no len()”,也就是说,函数length()中的len()会对传入的对象进行检验,如果不符合要求,就会报错,使用者可以根据报错信息对传入的对象类型进行调整。

在诸多介绍多态的文章中都会有关于“猫和狗”的例子。这里也将代码贴出来,读者去体会所谓多态体现。其实,如果你进入了Python的语境,有时不经意间就在应用多态特性。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. '''
  4. the code is from: http://zetcode.com/lang/python/oop/
  5. '''
  6.  
  7. __metaclass__ = type
  8.  
  9. class Animal:
  10. def __init__(self, name = ""):
  11. self.name = name
  12. def talk(self):
  13. pass
  14.  
  15. class Cat(Animal):
  16. def talk(self):
  17. print "Meow!"
  18.  
  19. class Dog(Animal):
  20. def talk(self):
  21. print "Woof!"
  22.  
  23. a = Animal()
  24. a.talk()
  25.  
  26. c = Cat("Missy")
  27. c.talk()
  28.  
  29. d = Dog("Rocky")
  30. d.talk()

保存后运行之:

  1. $ python 21101.py
  2. Meow!
  3. Woof!

代码中有Cat和Dog两个类,都继承了类Animal,它们都有talk()方法,输入不同的动物名称,会得出相应的结果。

关于多态,有一个被称作“鸭子类型”(duck typeing)的东西,其含义在维基百科中被表述为:

在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。这个概念的名字来源于James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

最后要提示读者,类型检查是毁掉多态的利器,比如type、isinstance以及isubclass函数,所以,一定要慎用这些类型检查函数。

4.6.2 封装和私有化

在正式介绍封装之前,先讲个笑话。

某软件公司老板号称自己懂技术。一次有一个项目要交付给客户,他不想让客户知道实现某些功能的代码,但是交付的时候必须要给人家代码。于是该老板就告诉程序员,“你们把那部分核心代码封装一下”。程序员听完迷茫了。

很多人没有笑,因为不明白说的是什么,不知道你有没有笑。这种幽默唯一的价值在于提到了一个词语“封装”。

“封装”是不是把代码写到某个东西里面,“人”在编辑器中打开也看不到呢?不是,除非你的显示器坏了。

在程序设计中,封装(Encapsulation)是对对象(object)的一种抽象,即将某些部分隐藏起来,在程序外部看不到,无法调用(不是人用眼睛看不到那个代码,除非用某种加密或者混淆方法,造成显示的是一堆混乱的代码,但这不是封装)。

要了解封装离不开“私有化”,就是将类或者函数中的某些属性限制在某个区域之内,外部无法调用,所以先说“私有化”。

“私有化”,顾名思义,就是将某个对象(这个对象可以是你认为的东西)限制在某个自己认定的范围内。比如,某国经济实行私有化,就是将经济体系中组成部分分别纳入到个人权限范畴,而不是放在众多个人无法触及的领域(那是公有),或者说,私有化就是产权清晰。与之对应的是“公有”。

Python中私有化的方法也比较简单,就是在准备私有化的属性(包括方法、数据)名字前面加双下画线。例如:

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. __metaclass__ = type
  5.  
  6. class ProtectMe:
  7. def __init__(self):
  8. self.me = "qiwsir"
  9. self.__name = "kivi"
  10.  
  11. def __python(self):
  12. print "I love Python."
  13.  
  14. def code(self):
  15. print "Which language do you like?"
  16. self.__python()
  17.  
  18. if __name__ == "__main__":
  19. p = ProtectMe()
  20. print p.me
  21. print p.__name

运行一下,看看效果:

  1. $ python 21102.py
  2. qiwsir
  3. Traceback (most recent call last):
  4. File "21102.py", line 21, in <module>
  5. print p.__name
  6. AttributeError: 'ProtectMe' object has no attribute '__name'

查看报错信息,告诉我们没有__name那个属性。果然隐藏了,在类的外面无法调用。再试试类里面的那个code()是否可以使用?把该程序做适当修改。

  1. if __name__ == "__main__":
  2. p = ProtectMe()
  3. p.code()
  4. p.__python()

修改好之后保存。其中p.code()的意图是要打印出两句话:“Which language do you like?”和“I love Python.”,code()方法和私有化的python()方法在同一个类中,按照私有化的含义,在类里面应该是可以调用的。而p.python()试图通过实例在类的外面调用它。看看效果:

  1. $ python 21102.py
  2. Which language do you like?
  3. I love Python.
  4. Traceback (most recent call last):
  5. File "21102.py", line 23, in <module>
  6. p.__python()
  7. AttributeError: 'ProtectMe' object has no attribute '__python'

如愿以偿,该调用的调用了,该隐藏的隐藏了。

用上面的方法的确做到了封装。但是,如果要调用那些私有属性怎么办?

可以使用property函数。请看下面的例子:

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. __metaclass__ = type
  5.  
  6. class ProtectMe:
  7. def __init__(self):
  8. self.me = "qiwsir"
  9. self.__name = "kivi"
  10.  
  11. @property
  12. def name(self):
  13. return self.__name
  14.  
  15. if __name__ == "__main__":
  16. p = ProtectMe()
  17. print p.name

运行结果:

  1. $ python 21102.py
  2. kivi

从上面可以看出,用了@property之后,再调用那个方法的时候,用p.name的形式,就好像在调用以往非私有化属性一样。

看来,封装的确不是“让人看不见”。