爱是恒久忍耐,又有恩慈;爱是不嫉妒,爱是不自夸,不张狂,不作害羞的事,不求自己的益处,不轻易发怒,不计算人的恶,不喜欢不义,只喜欢真理;凡事包容,凡事相信,凡事盼望,凡事忍耐。(1 CORINTHIANS 13:4-7)

多态和封装

“多态”和“封装”是OOP的重要特征——前面说的“继承”也是。但是,对于Python而言,对这两个的理解也有很多不同。建议读者“吃百家宴”,到网上搜一搜有关话题,不少人写了文章来讨论。

多态

这里我仅仅针对初学者,按照自己的理解,谈谈零基础学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函数吗?如果忘记了,请复习函数(5)中对此的解释。

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

这里我们没有限制参数的类型,也一定不能限制,因为如果限制了,就不是pythonic了。在使用的时候,可以给参数任意类型,都能得到不报错的结果。

以上,就体现了“多态”——同一种行为具有不同表现形式和形态的能力,换一种说法,就是对象多种表现形式的体现。

当然,也有人就此提出了反对意见,因为本质上是在参数传入值之前,Python并没有确定参数的类型,只能让数据进入函数之后再处理,能处理则罢,不能处理就报错。例如:

>>> 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()

上述的种种多态表现,皆因为Python是一种解释型的语言,不需要进行预编译,只在运行时才确定状态。所以,Python就被认为天生是一种多态的语言。也有人持相反观点,认为Python不支持多态,在理由中,也用了上述内容。看来,看着半杯水,的确能够有不同的结论——“还有半杯水呢!”和“还剩半杯水了!”。

争论,让给思想者。我们,围观。

为了让读者能够进一步理解Python的多态特点,必须要比较,不跟世界上三分之二尚处于水深火热的劳苦大众比较,怎能体会到自己生活在幸福的祖国大花园内。

《Thinking in Java》的作者Bruce Eckel在2003年5月2日发表了一篇题为《Strong Typing vs. Strong Testing》的博客,在其中将Java和Python的多态特征进行了比较,在此我选摘部分内容,重温大师的论述。

先来欣赏大师所撰写的一段Java代码:

// Speaking pets in Java:
interface Pet {
  void speak();
}

class Cat implements Pet {
  public void speak() { System.out.println("meow!"); }
}

class Dog implements Pet {
  public void speak() { System.out.println("woof!"); }
}

public class PetSpeak {
  static void command(Pet p) { p.speak(); }
  public static void main(String[] args) {
    Pet[] pets = { new Cat(), new Dog() };
    for(int i = 0; i < pets.length; i++)
      command(pets[i]);
  }
}

如果读者没有学习过Java,对上述代码理解可能不是很顺畅,这不重要。主要观察command(Pet p),这种写法意味着函数command()所能接受的参数的类型必须是Pet类型,其它类型不行。所以,必须创建interface Pet这个接口并且类Cat和Dog继承它,然后才能upcast them to the generic command() method。(原文: I must create a hierarchy of Pet, and inherit Dog and Cat so that I can upcast them to the generic command() method.)

与上面的代码相对应,大师提供了Python代码,如下所示:

# Speaking pets in Python:
class Pet:
    def speak(self): pass

class Cat(Pet):
    def speak(self):
        print "meow!"

class Dog(Pet):
    def speak(self):
        print "woof!"

def command(pet):
    pet.speak()

pets = [ Cat(), Dog() ]

for pet in pets:
    command(pet)

注意这段Python代码中的command()函数,其参数pet并没有要求必须是前面的Pet类型(注意区分大小写),仅仅是一个名字为pet的对象引用罢了。Python不关心引用的对象是什么类型,只要改对象有speak()方法即可。提醒读者注意的是,因为历史原因(2003年),大师当时写的是旧式类。

根据我们对Python的理解,上面代码中的类Pet其实是多余的。是的,大师也这么认为,只是因为大师当时是完全模仿Java程序而写的。随后,大师就修改了上面的代码。

# Speaking pets in Python, but without base classes:
class Cat:
    def speak(self):
        print "meow!"

class Dog:
    def speak(self):
        print "woof!"

class Bob:
    def bow(self):
        print "thank you, thank you!"
    def speak(self):
        print "hello, welcome to the neighborhood!"
    def drive(self):
        print "beep, beep!"

def command(pet):
    pet.speak()

pets = [ Cat(), Dog(), Bob() ]

for pet in pets:
    command(pet)

不仅去掉了没什么用的类Pet,又增加了一个新的类Bob,这个类根本不是如CatDog那样的类型,只是它碰巧也有一个名字为speak()的方法罢了。但是,也依然能够在command()函数中被调用。

这就是Python中的多态特点,大师Brue Eckel通过非常有说明了的代码说明了Java和Python的区别,并充分展示了Python中的多态特征。

诚如前面所述,Python不检查传入对象的类型(上面大师所写的代码中非常清晰表明了这点),这种方式被称之为“隐式类型”(laten typing)或者“结构式类型”(structural typing),也被通俗的称为“鸭子类型”(duck typeing),其含义在维基百科中被表述为:

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

鸭子类型就意味着可以向任何对象发送任何消息,语言只关心该对象能否接受该消息,不强求该对象是否某一种特定的类型——该对象的多态表现。

对于Python的这种特征,有一批程序员不接受,他们认为在程序被执行的时候,可能收到错误的对象,而且这种错误还可能潜伏在程序的某个角落。因此在编程领域就有了“强类型”(如Java)和“弱类型”(如Python)之争。

对于此类争论,大师Brue Eckel在上面所提到的博客中,给出了非常明确的回答。下面原文恭录于此:

Strong testing, not strong typing.

So this, I assert, is an aspect of why Python works. C++ tests happen at compile time (with a few minor special cases). Some Java tests happen at compile time (syntax checking), and some happen at run time (array-bounds checking, for example). Most Python tests happen at runtime rather than at compile time, but they do happen, and that's the important thing (not when). And because I can get a Python program up and running in far less time than it takes you to write the equivalent C++/Java/C# program, I can start running the real tests sooner: unit tests, tests of my hypothesis, tests of alternate approaches, etc. And if a Python program has adequate unit tests, it can be as robust as a C++, Java or C# program with adequate unit tests (although the tests in Python will be faster to write).

读大师的话,醍醐灌顶,豁然开朗,再也不去参与那些浪费唾沫的争论了。

顺便再告诉读者,从发表于2003年5月2日的《Strong Typing vs. Strong Testing》中可以看出,大师在那时已经开始在授课的过程中给学生使用Python了。2003年,那时候赵国程序员,有多少知道这个星球上有一种名为Python的计算机高级语言。

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

封装和私有化

“封装”,是不是把代码写到某个东西里面,“人”在编辑器中打开,就看不到了呢?

除非是你的显示器坏了。

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

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

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

#!/usr/bin/env python
# coding=utf-8

class ProtectMe(object):        #Python 3: class ProtectMe:
    def __init__(self):
        self.me = "qiwsir"
        self.__name = "kivi"

    def __python(self):
        print "I love Python."        #Python 3: 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那个属性。果然隐藏了,在类的外面无法调用。再试试那个函数,可否?

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

class ProtectMe(object):    #Python 3: 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 3: print(p.name)

运行结果:

$ python 21102.py 
kivi

从上面可以看出,用了@property之后,在调用那个方法的时候,用的是p.name的形式,就好像在调用一个属性一样,跟前面p.me的格式相同。

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


总目录   |   上节:类(5)   |   下节:定制类

如果你认为有必要打赏我,请通过支付宝:[email protected],不胜感激。

results matching ""

    No results matching ""