当前位置: 首页 > 文档资料 > Python 精要教程 >

第七章 更加抽象

优质
小牛编辑
126浏览
2023-12-01

前几章介绍了Python主要的内建对象类型(数字、字符串、列表、元组和字典),以及内建函数和标准库的用法,还有定义函数的方法。现在看来,还差一点——创建自己的对象。这正是本章要介绍的内容。

为什么要自定义对象呢?建立自己的对象类型可能很酷,但是做什么用呢?使用字典、序列、数字和字符串来创建函数,完成这项工作还不够吗?这样做当然可以,但是创建自己的对象(尤其是类型或者被称为的对象)是Python的核心概念——非常核心,事实上,Python被成为面向对象的语言(和SmallTalk、C++、Java以及其他语言一样)。本章将会介绍如何创建对象,以及多态、封装、方法、特性、超类以及继承的概念——新知识很多。那么我们开始吧。

注:熟悉面向对象程序设计概念的读者也应该了解构造函数。本章不会提到构造函数,关于它的完整讨论,请参见第九章。

7.1 对象的魔力

在面向对象程序设计中,术语对象(object)基本上可以看做数据(特性)以及由一系列可以存取、操作这些数据的方法所组成的集合。使用对像代替全局变量和函数的原因可能有很多。其中对象最重要的有点包括以下几方面。

☑ 多态(Polymorphism):意味着可以对不同类的对象使用同样的操作,它们会像被“施了魔法一般”工作。

☑ 封装(Encapsulation):对外部世界隐藏对象的工作细节。

☑ 继承(Inheritance):以通用的类为基础建立专门的对象。

在许多关于面向对象程序设计的介绍中,这几个概念的顺序是不同的。封装和继承会首先被介绍,因为它们被用作现实世界中的对象的模型。这种方法不错,但是在我看来,面向对象程序设计最有趣的特性是多态。(以我的经历来看)它也是让大多数人犯晕的特性。所以本章会以多态开始,而且这一个概念就足以让你喜欢面向对象程序设计了。

7.1.1 多态

术语多态来自希腊语,意思是“有多种形式”。多态意味着就算不知道变量所引用的对象类型是什么,还是能对它进行操作,而它也会根据对象(或类)类型的不同而表现出不同的行为。例如,假设一个食品销售的商业网站创建了一个在线支付系统。程序会从系统的其他部分(或者以后可能会设计的其他类似的系统)获得一“购物车”中的商品,接下来要做的就是算出总价然后使用信用卡支付。

当你的程序获得商品时,首先想到的可能是如何具体地表示它们。比如需要将它们作为元组接收,像下面这样:

("SPAM", 2.50)

如果需要描述性标签和价格,这样就够了。但是这个程序还是不够灵活。我们假设网站支持拍卖服务,价格在货物卖出之前会逐渐降低。如果用户能够把对象放入购物车,然后处理结账(你的系统部分),等价格到了满意的程度后按下“支付”按钮就好了。

但是这样一来简单的元组就不能满足需要了。为了实现这个功能,代码每次询问价格的时候,对象都需要检查当前的价格(通过网络的某些功能),价格不能固定在元组中。解决起来不难,只要写个函数:

# Don't do it
def getPrice(object): 
    if isinstance(object, tuple): 
        return object[1] 
    else: 
        return magic_network_method(object)

注:这里用isinstance进行类型/类检查是为了说明一点,类型检查一般来说并不是什么好方法,能不用则不用。函数isinstance在7.2.6节会介绍。

前面的代码中使用isinstance函数查看对象是否为元组。如果是的话,就返回它的第2个元素,否则会调用一些“有魔力的”网络方法。

假设网络功能部分已经存在,那么问题已经解决了,目前为止是这样。但程序还不是很灵活。如果某些聪明的程序员决定用十六进制数的字符串来表示价格,然后存储在字典中的键"price"下面呢?没问题,只要更新函数:

# Don't do it
def getPrice(object): if isinstance(object, tuple): return object[1] elif isinstance(object, dict): return int(objecct["price"]) else: return magic_network_method(object)

现在是不是已经考虑到了所有的可能性?但是如果某些人希望为存储在其他键下面的价格增加新的字典呢?那有怎么办呢?可以再次更新getPrice函数,但是这种工作还要做多长时间?每次有人要实现价格对象的不同功能时,都要再次实现你的模块。但是如果这个模块已经卖出了并且转到了其他更酷的项目中,那要怎么应付客户?显然这是个不灵活且不切实际的实现多种行为的代码编写方式。

那么应该怎么办?可以让对象自己进行操作。听起来很清楚,但是想一下,这样做会轻松很多。每个新的对象类型都可以检索和计算自己的价格并且返回结果,只需向它询问价格即可。这时候多态(在某种程度上还有封装)就要出场了。

1. 多态和方法

程序接收到一个对象,完全不了解该对象的内部实现方式——它可能有多种“形状”。你要做的就是询问价格,这样就够了,实现方法是我们熟悉的:

>>> object.getPrice() 
2.5

绑定到对象特性上面的函数成为方法(method)。我们已经见过字符串、列表和字典方法。实际上多态也已经出现过:

>>> "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") 1

本例中,看来是字符串胜出了(Marlowes:原文上随机选择到的是字符串。 =_=)。但是关键点在于不需要检测类型:只需要知道x有个叫做count的方法,带有一个字符作为参数,并且返回整数值就够了。如果其他人创建的对象类也有count方法,那也无所谓,你只需要像用字符串和列表一样使用该对象就行了。

2. 多态的多种形式

任何不知道对象到底是什么类型,但是又要对对象“做点儿什么”的时候,都会用到多态。这不仅限于方法,很多内建运算符和函数都有多态的性质,考虑下面这个例子:

>>> 1 + 2
3
>>> "Fish" + "license"
'Fishlicense'

这里的加运算符对于数字(本例中为整数)和字符串(以及其他类型的序列)都能起作用。为说明这一点,假设有个叫做add的函数,它可以将两个对象相加。那么可以直接将其定义成上面的形式(功能等同但比operator模块中的add函数效率低些)。

>>> def add(x, y):
...     return x + y 
# 对于很多类型的参数都可以用:
>>> add(1, 2) 
3
>>> add("Fish", "license") 
'Fishlicense'

看起来有些傻,但是关键在于参数可以是任何支持加法的对象(注意,这类对象只支持同类的加法。调用add(1, "license")不会起作用)。如果需要编写打印对象长度消息的函数,只需要对象具有长度(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

很多函数和运算符都是多态的——你写的绝大多数程序可能都是,即便你并非有意这样。只要使用多态函数和运算符,就会与“多态”发生关联。事实上,唯一能够毁掉多态的就是使用函数显式地检查类型,比如typeisinstance以及issubclass函数等。如果可能的话,应该尽力避免使用这些毁掉多态的方式。真正重要的是如何让对象按照你所希望的方式工作,不管它是否是正确的类型(或者类)。

注:这里所讨论的多态的形式是Python式编程的核心,也是被成为“鸭子类型”(duck typing)的东西。这个名词出自俗语“如果它像鸭子一样呱呱大叫······”。有关它的更多信息,请参见 http://en.wikipedia.org/wiki/Duck_typing

7.1.2 封装

封装是指向程序中的其他部分隐藏对象的具体实现的原则。听起来有些像多态,也是使用对象而不用知道其内部细节,两者概念有些类似,因为它们都是抽象的原则,它们都会帮助处理程序组件而不用过多关心多余细节,就像函数做的一样。

但是封装并不等同于多态。多态可以让用户对于不知道是什么类(对象类型)的对象进行方法调用,而封装是可以不用关心对象是如何构建的而直接进行调用。听起来还是有些相似?让我们用多态而不用封装写个例子,假设有个叫做OpenObject的类(本章后面会学到如何创建类):

>>> o = OpenObject()  
# This is how we create objects...
>>> o.setName("Sir Lancelot") 
>>> o.getName()
'Sir Lancelot'

创建了一个对象(通过像调用函数一样调用类)后,将变量o绑定到该对象上。可以使用setNamegetName方法(假设已经由OpenObject类提供)。一切看起来都很完美。但是假设变量o将它的名字存储在全局变量globalName中:

>>> globalName 
"Sir Lancelot"

这就意味着在使用OpenObject类的实例时候,不得不关心globalName的内容。实际上要确保不会对它进行任何更改:

>>> globalName = "Sir XuHoo"
>>> o.getName() 
'Sir XuHoo'

如果创建了多个OpenObject实例的话就会出现问题,因为变量相同,所以可能会混淆:

>>> o1 = OpenObject() 
>>> o2 = OpenObject() 
>>> o1.setName("Robin Hood") 
>>> o2.getName() 
'Robin Hood'

可以看到,设定一个名字后,其他的名字也就自动设定了。这可不是想要的结果。

基本上,需要将对象进行抽象,调用方法的时候不用关心其他的东西,比如它是否干扰了全局变量。所以能将名字“封装”在对象内吗?没问题。可以将其作为特性(attribute)存储。

正如方法一样,特性是作为变量构成对象的一部分,事实上方法更像是绑定到函数上的属性(在本章的7.2.3节中会看到方法和函数重要的不同点)。

如果不用全局变量而用特性重写类,并且重命名为ClosedObject,它会像下面这样工作:

>>> c = ClosedObject() 
>>> c.setName("Sir Lancelot") 
>>> c.getName() 
'Sir Lancelot'

目前为止还不错。但是,值可能还是存储在全局变量中的。那么再创建另一个对象:

>>> r = ClosedObject() 
>>> r.setName("Sir Robin") 
>>> r.getName() 
'Sir Robin'

可以看到新的对象的名称已经正确设置。这可能正是我们期望的。但是第一个对象怎么样了呢?

>>> c.getName() 
'Sir Lancelot'

名字还在!这是因为对象有它自己的状态(state)。对象的状态由它的特性(比如名称)来描述。对象的方法可以改变它的特性。所以就像是将一大堆函数(方法)捆在一起,并且给予它们访问变量(特性)的权力,它们可以在函数调用之间保持保存的值。

本章后面的“再论私有化”一节也会对Python的封装机制进行更详细的介绍。

7.1.3 继承

继承是另外一个懒惰(褒义)的行为。程序员不想把同一段代码输入好几次。之前使用的函数避免了这种情况,但是现在又有个更微妙的问题。如果已经有了一个类,而又想建立一个非常类似的呢?新的类可能只是添加几个方法。在编写新类时,又不想把旧类的代码全都复制过去。

比如说有个Shape类,可以用来在屏幕上画出指定的形状。现在需要创建一个叫做Rectangle的类,它不但可以在屏幕上画出指定的形状,而且还能计算该形状的面积。但又不想把Shape里面已经写好的draw方法再写一次。那么该怎么办?可以让RectangleShape继承方法。在Rectangle对象上调用draw方法时,程序会自动从Shape类调用该方法。(参见7.2.5节)。

7.2 类和类型

现在读者可能对什么是类有了大体感觉——或者已经有些不耐烦听我对它进行更多介绍了。在开始介绍之前,先来认识一下什么是类,以及它和类型又有什么不同(或相同)。

7.2.1 类到底是什么

前面的部分中,类这个词已经多次出现,可以将它或多或少地视为种类或者类型的同义词。从很多方面来说,这就是类——一种对象。所有的对象都属于某一个类,称为类的实例(instance)。

例如,现在请往窗外看,鸟就是“鸟类” 的实例。鸟类是一个非常通用(抽象)的类,具有很多子类:看到的鸟可能属于子类“百灵鸟”。可以将“鸟类”想象成所有鸟的集合,而“百灵鸟类”是其中的一个子集。当一个对象所属的类是另外一个对象所属类的子集时,前者就被成为后者的子类(subclass),所以“百灵鸟类”是“鸟类”的子类。相反,“鸟类”是“百灵鸟类”的超类(superclass)。

注:日常交谈中,可能经常用复数来描述对象的类,比如birds或者larkes。Python中,习惯上都使用单数名词,并且首字母大写,比如BirdLark

这样一比喻,子类和超类就容易理解了。但是在面向对象程序设计中,子类的关系是隐式的,因为一个类的定义取决于它所支持的方法。类的所有实例都会包含这些方法,所以所有子类的所有实例都有这些方法。定义子类只是个定义更多(也有可能是重载已经存在的)的方法的过程。

例如,鸟类Bird可能支持fly方法,而企鹅类Penguin(Bird的子类)可能会增加个eatFish方法。当创建Penguin类时,可能会想要重写(override)超类的fly方法,对于Penguin的实例来说,这个方法要么什么也不做,要么就产生异常(参见第8章),因为penguin(企鹅)不会fly(飞)。

注:在旧版本的Python中,类和类型之间有很明显的区别。内建的对象是基于类型的,自定义的对象则是基于类的。可以创建类但是不能创建类型。最近版本的Python中,事情有了些变化。基本类型和类之间的界限开始模糊了。可以创建内建类型的子类(或子类型),而这些类型的行为更类似于类。在越来越熟悉这门语言后会注意到这一点。如果感兴趣的话,第九章中会有关于这方面的更多信息。

7.2.2 创建自己的类

终于来了!可以创建自己的类了!先来看一个简单的类:

# 确定使用新式类
__metaclass__ = type  

class Person: 
    def setName(self, name):
        self.name = Name 
    def getName(self): 
        return self.name 
    def greet(self): 
        print "Hello, world! I'm %s" % self.name

注:所谓的旧式类和新式类之间是有区别的。除非是Python3.0之前版本中默认附带的代码,否则再继续使用旧式类已无必要。新式类的语法中,需要在模块或者脚本开始的地方放置赋值语句__metaclass__ = type(并不会在每个例子中显式地包含这行语句)。除此之外也有其他的方法,例如继承新式类(比如object)。后面马上就会介绍继承的知识。在Python3.0中,旧式类的问题不用再担心,因为它们根本就不存在了。请参见第九章获取更多信息。

这个例子包含3个方法定义,除了它们是写在class语句里面外,一切都像是函数定义。Person当然是类的名字。class语句会在函数定义的地方创建自己的命名空间(参见7.2.4节)。一切看起来都挺好,但是那个self参数看起来有点奇怪。它是对于对象自身的引用。那么它是什么对象?让我们创建一些实例看看:

>>> foo = Person() 
>>> bar = Person() 
>>> foo.setName("Luke Skywalker") 
>>> bar.setName("Anakin Skywalker") 
>>> foo.greet()
Hello, world! I'm Luke Skywalker
>>> bar.greet()
Hello, world! I'm Anakin Skywalker

好了,例子一目了然,应该能说明self的用处了。在调用foosetNamegreet函数时,foo自动将自己作为第一个参数传入函数中——因此形象的命名为self。对于这个变量,每个人可能都会有自己的叫法,但是因为它总是对象自身,所以习惯上总是叫做self

显然这就是self的用处和存在的必要性。没有它的话,成员方法就没法访问他们要对其特性进行操作的对象本身了。

和之前一样,特性是可以在外部访问的:

>>> foo.name 'Luke Skywalker'
>>> bar.name = "Yoda"
>>> bar.greet()
Hello, world! I'm Yoda

注:如果知道fooPerson的实例的话,那么还可以把foo.greet()看作Person.greet(foo)方便的简写。

7.2.3 特性、函数和方法

(在前面提到的)self参数事实上正是方法和函数的区别。方法(更专业一点可以成为绑定方法)将它们的第一个参数绑定到所属的实例上,因此您无需显式提供该参数。当然也可以将特性绑定到一个普通函数上,这样就不会有特殊的self参数了:

>>> class Class:
...     def method(self):
...         print "I have a self!"

>>> def function():
...     print "I don't..."

>>> instance = Class() 
>>> instance.method()
I have a self! 
>>> instance.method = function 
>>> instance.method()
I don't...

注意,self参数并不依赖于调用方法的方式,前面我们使用的是instance.method(实例.方法)的形式,可以随意使用其他变量引用同一个方法:

>>> class Bird:
...     song = "Squaawk!" 
...     def sing(self):
...         print self.song 
>>> bird = Bird() 
>>> bird.sing()
Squaawk! 
>>> brid.song 
>>> birdsong = bird.sing()
Squaawk!

尽管最后一个方法调用起来与函数调用十分相似,但是变量birdsong引用绑定方法(第九章中,将会介绍类是如何调用超类方法的(具体来说就是超类的构造器)。这些方法直接通过类调用,他们没有绑定自己的self参数到任何东西上,所以叫做非绑定方法)bird.sing上,也就意味着这还是会对self参数进行访问(也就是说,它仍旧绑定到类的相同实例上)。

再论私有化

默认情况下,程序可以从外部访问一个对象的特性。再次使用前面讨论过的相关封装的例子:

>>> c.name 'Sir Lancelot'
>>> c.name = 'Sir Gumby'
>>> c.getName() 'Sir Gumby'

有些程序员觉得这样做是可以的,但是有些人(比如SmallTalk之父,SmallTalk的对象特性只允许由同一个对象的方法访问)觉得这样做就破坏了封装的原则。他们认为对象的状态对于外部应该是完全隐藏(不可访问)的。有人可能会奇怪为什么他们会站在如此极端的立场上。每个对象管理自己的特性还不够吗?为什么还要对外部世界隐藏呢?毕竟如果能直接使用ClosedObjectname特性的话就不用使用setNamegetName方法了。

关键在于其他程序员可能不知道(可能也不应该知道)你的对象内部的具体操作。例如,ClosedObject可能会在其他对象更改自己的名字的时候,给一些管理员发送邮件消息。这应该是setName方法的一部分。但是如果直接使用c.name设定名字会发生什么?什么都没发生,Email也没发出去。为了避免这类事情的发生,应该使用私有(private)特性,这是外部对象无法访问,但getNamesetName访问器(accessor)能够访问的特性。

注:第九章中,将会介绍有关属性(property)的只是,它是访问器最有力的替代者。

Python并不直接支持私有方式,而是要靠程序员自己把握在外部进行特性修改的时机。毕竟在使用对象前应该知道如何使用。但是,可以用一些小技巧达到私有特性的效果。

为了让方法或者特性变为私有(从外部无法访问),只要在它的名字前面加上双下划线即可:

class Secretive(): 
    def __inaccessible(self): 
        print "Bet you can't see me..."

    def accessible(self): 
        print "The secret message is:" 
        self.__inaccessible()

现在__inaccessible从外界是无法访问的,而在类内部还能使用(比如从accessible)访问:

>>> s = Secretive() 
>>> s.__inaccessible()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module> 
  AttributeError: Secretive instance has no attribute '__inaccessible'
>>> s.accessible()
The secret message is:
Bet you can't see me...

尽管双下划线有些奇怪,但是看起来像是其他语言中的标准的私有方法。真正发生的事情才是不标准的。类的内部定义中,所有以双下划线开始的名字都被“翻译”成前面加上单下划线和类名的形式:

>>> Secretive._Secretive__inaccessible <unbound method Secretive.__inaccessible>

在了解这些幕后的事情后,实际上还能在类外访问这些私有方法,尽管不应该这么做:

>>> s._Secretive__inaccessible()
Bet you can't see me...

简而言之,确保其他人不会访问对象的方法和特性是不可能的,但是这类”名称变化术“是他们不应该访问这些函数或者特性的强有力信号。

如果不需要使用这种方法但是又想让其他对象不要访问内部数据,那么可以使用单下划线。这不过是个习惯,但的确有实际效果。例如,前面有下划线的名字都不会被带星号的import语句(from module import *)导入(有些语言支持多层次的成员变量(特性)私有性。比如Java就支持4种级别。尽管单下划线在某种程度上给出两个级别的私有性,但Python并没有真正的私有化支持)。

7.2.4 类的命名空间

下面的两个语句(几乎)等价:

def foo(x): 
    return x * x
foo = lambda x: x * x

两者都创建了返回参数平方的函数,而且都将变量foo绑定到函数上。变量foo可以在全局(模块)范围进行定义,也可处于局部的函数或方法内。定义类时,同样的事情也会发生,所有位于class语句中的代码都在特殊的命名空间中执行——类命名空间(class namespace)。这个命名空间可由类内所有成员访问。并不是所有Python程序员都知道类的定义其实就是执行代码块,这一点非常有用,比如,在类的定义区并不只限定只能使用self语句:

>>> class C:
...     print "Class C being defined..." 
...
Class C being defined... 
>>>

看起来有点傻,但是看看下面的:

>>> class MemberCounter:
...     members = 0
...     def init(self):
...         MemberCounter.members += 1 
... 
>>> m1 = MemberCounter() 
>>> m1.init() 
>>> MemberCounter.members 
1
>>> m2 = MemberCounter() 
>>> m2.init() 
>>> MemberCounter.members 
2

上面的代码中,在类作用域内定义了一个可供所有成员(实例)访问的变量,用来计算类的成员数量。注意init用来初始化所有实例:第九章中,我会让这一过程自动化(即把它变成一个适当的构造函数)。

就像方法一样,类作用域内的变量也可以被所有实例访问:

>>> m1.members 2
>>> m2.members 2

那么在实例中重绑定members特性呢?

>>> m1.members = "Two"
>>> m1.members 'Two'
>>> m2.members 
2

members值被写到了m1的特性中,屏蔽了类范围内的变量。这跟函数内的局部和全局变量的行为十分类似,就像第六章讨论的”屏蔽的问题“。

7.2.5 指定超类

就像本章前面我们讨论的一样,子类可以扩展超类的定义。将其他类名写在class语句后的圆括号内可以指定超类:

class Filter(): 
    def init(self):
        self.blocked = [] 
        def filter(self, sequence): 
            return [x for x in sequence if x not in self.blocked] 
class SPAMFilter(Filter):  
    # SPAMFilter是Filter的子类

    def init(self):  
        # 重写Filter超类中的init方法
        self.blocked = ["SPAM"]

Filter是个用于过滤序列的通用类,事实上它不能过滤任何东西:

>>> f = Filter() 
>>> f.init() 
>>> f.filter([1, 2 ,3])
[1, 2, 3]

Filter类的用处在于它可以用作其他类的基类(超类),比如SPAMFilter类,可以将序列中的'SPAM'过滤出去。

>>> s = SPAMFilter() 
>>> s.init() 
>>> s.filter(["SPAM", "SPAM", "SPAM", "SPAM", "eggs", "bacon", "SPAM"])
['eggs', 'bacon']

注意SPAMFilter定义的两个要点。

☑ 这里用提供新定义的方式重写了Filterinit定义。

filter方法的定义是从Filter类中拿过来(继承)的,所以不用重写它的定义。

第二个要点揭示了继承的用处:我可以写一大堆不同的过滤类,全部都从Filter继承,每一个我都可以使用已经实现的filter方法。这就是前面提到过的有用的懒惰。

7.2.6 检查继承

如果想要查看一个类是否是另一个的子类,可以使用内建的issubclass函数:

>>> issubclass(SPAMFilter, Filter)
True 
>>> issubclass(Filter, SPAMFilter)
False

如果想要知道已知的基类(们),可以直接使用它的特殊特性__bases__

>>> SPAMFilter.__bases__ (<class __main__.Filter at 0x7fa160e4a4c8>,) 
>>> Filter.__bases__ ()

同样,还能用使用isinstance方法检查一个对象是否是一个类的实例:

>>> s = SPAMFilter() 
>>> isinstance(s, SPAMFilter)
True 
>>> isinstance(s, Filter)
True 
>>> isinstance(s, str)
False

注:使用isinstance并不是个好习惯,使用多态会更好一些。

可以看到,s是SPAMFilter类的(直接)实例,但是它也是Filter类的间接实例,因为SPAMFilterFilter的子类。另外一种说法就是SPAMFilter类就是Filters类。可以从前一个例子中看到,isinstance对于类型也起作用,比如字符串类型(str)。

如果只想知道一个对象属于哪个类,可以使用__class__特性:

>>> s.__class__
<class __main__.SPAMFilter at 0x7fa160e4a530>

注:如果使用__metaclass__ = type或从object继承的方式来定义新式类,那么可以使用type(s)查看实例所属的类。

7.2.7 多个超类

可能有的读者注意到了上一节中的代码有些奇怪:也就是__bases__这个复数形式。而且文中也提到过可以找到一个新的基类(们),也就按暗示它的基类可能会多余一个。事实上就是这样,建立几个新的类来试试看:

class Calculator: 
    def calculate(self, expression):
        self.value = eval(expression) 
class Talker: 
    def talk(self): 
        print "Hi, my value is", self.value 
class TalkingCalculator(Calculator, Talker): 
    pass

子类(TalkingCalculator)自己不做任何事,它从自己的超类继承所有的行为。它从Calculator类那里继承calculate方法,从Talker类那里继承talk方法,这样它就成了会说话的计算器(talking calculator)。

>>> tc = TalkingCalculator() 
>>> tc.calculate("1 + 2 * 3") 
>>> tc.talk()
Hi, my value is 7

这种行为称为多重继承(multiple inheritance),是个非常有用的工具。但除非读者特别熟悉多重继承,否则应该尽量避免使用,因为有些时候会出现不可预见的麻烦。

当使用多重继承时,有个需要注意的地方。如果一个方法从多个超类继承(也就是说你有两个具有相同名字的不同方法),那么必须要注意一下超类的顺序(在class语句中):先继承的类中的方法会重写后继承的类中的方法。所以如果前例中Calculator类也有个叫做talk的方法,那么它就会重写Talkertalk方法(使其不可访问)。如果把它们的顺序调过来,像下面这样:

class TalkingCalculator(Talker, Calculator): 
    pass

就会让Talkertalk方法可用了。如果超类们共享一个超类,那么在查找给定方法或者属性时访问超的顺序称为MRO(Method Resolution Order, 方法判定顺序),使用的算法相当复杂。幸好,它工作得很好,所以不用过多关心。

7.2.8 接口与内省

“接口”的概念与多态有关。在处理多态对象时,只要关心它的接口(或称“协议”)即可,也就是公开的方法和特性。在Python中,不用显式地指定对象必须包含哪些方法才能作为参数接收。例如,不用(像在Java中一样)显式地编写接口,可以在使用对象的时候假定它可以实现你所要求的行为。如果它不能实现的话,程序就会失败。

一般来说只需要让对象符合当前的接口(换句话说就是实现当前方法),但是还可以更灵活一些。除了调用方法然后期待一切顺利之外,还可检查所需方法是否已经存在。如果不存在,就需要做些其他事情:

>>> hasattr(tc, "talk")
True 
>>> hasattr(tc, "fnord")
False

注:callable函数在Python3.0中已不再可用。可以使用hasattr(x, "__call__")来代替callable(x)

这段代码使用了getattr函数,而没有在if语句内使用hasattr函数直接访问特性,getattr函数允许提供默认值(本例中为None),以便在特性不存在时使用,然后对返回的对象使用callable函数。

注:与getattr相对应的函数是setattr,可以用来设置对象的特性:

>>> setattr(tc, "name", "Mr. XuHoo") 
>>> tc.name 'Mr. XuHoo'

如果要查看对象内所有存储的值,那么可以使用__dict__特性。如果真的想要找到对象是由什么组成的,可以看看inspect模块。这是为那些想要编写对象浏览器(以图形方式浏览Python对象的程序)以及其他需要类似功能的程序的高级用户准备的。关于对象和模块的更多信息,可以参见10.2节。

7.3 一些关于面向对象设计的思考

关于面向对象设计的书籍已经有很多,尽管这并不是本书所关注的主题,但是还是给出一些要点。

☑ 将属于一类的对象放在一起。如果一个函数操纵一个全局变量,那么两者最好都在类内作为特性和方法出现。

☑ 不要让对象过于亲密。方法应该只关心自己实例的特性。让其他实例管理自己的状态。

☑ 要小心继承,尤其是多重继承。继承机制有时很有用,但也会在某些情况下让事情变得过于复杂。多继承难以正确使用,更加难以调试。

☑ 简单就好。让你的方法小巧。一般来说,多数方法都应能在30秒内被读完(以及理解),尽量将代码行数控制在一页或者一屏之内。

当考虑需要什么类以及类要有什么方法时,应该尝试下面的方法。

(1)写下问题的描述(程序要做什么),把所有的名词、动词和形容词加下划线。

(2)对于所有名词,用作可能的类。

(3)对于所有动词,用作可能的方法。

(4)对于所有形容词,用作可能的特性。

(5)把所有方法和特性分配到类。

现在已经有了面向对象模型的草图了。还可以考虑类和对象之间的关系(比如继承或协作)以及它们的作用,可以用以下步骤精炼模型。

(1)写下(或者想象)一系列的使用实例,也就是程序应用时的场景,试着包括所有的功能。

(2)一步步考虑每个使用实例,保证模型包括所有需要的东西。如果有些遗漏的话就添加进来。如果某处不太正确则改正。继续,直到满意为止,

当认为已经有了可以应用的模型时,那就可以开工了。可能需要修正自己的模型,或者是程序的一部分。幸好,在Python中不用过多关心这方面的事情,因为很简单,只要投入进去就行(如果需要面向对象程序设计方面的更多指导,请参见第十九章推荐的书目)。

7.4 小结

本章不仅介绍了更多关于Python语言的信息,并且介绍了几个可能完全陌生的概念。下面总结一下。

☑ 对象:对象包括特性和方法。特性只是作为对象的一部分变量,方法则是存储在对象内的函数。(绑定)方法和其他函数的区别在于方法总是将对象作为自己的第一个参数,这个参数一般称为self。

☑ 类:类代表对象的集合(或一类对象),每个对象(实例)都有一个类。类的主要任务是定义它的实例会用到的方法。

☑ 多态:多态是实现将不同类型和类的对象进行同样对待的特性——不需要知道对象属于哪个类就能调用方法。

☑ 封装:对象可以将它们的内部状态隐藏(或封装)起来。在一些语言中,这意味着对象的状态(特性)只对自己的方法可用。在Python中,所有的特性都是公开可用的,但是程序员应该在直接访问对象状态时谨慎行事,因为他们可能无意中使得这些特性在某些方面不一致。

☑ 继承:一个类可以是一个或者多个类的子类。子类从超类继承所有方法。可以使用多个超类,这个特性可以用来组成功能的正交部分(没有任何联系)。普通的实现方式是使用核心的超类和一个或者多个混合的超类。

☑ 接口和内省:一般来说,对于对象不用探讨过深。程序员可以靠多态调用自己需要的方法。不过如果想要知道对象到底有什么方法和特性,有些函数可以帮助完成这项工作。

☑ 面向对象设计:关于如何(或者说是否应该进行)面向对象设计有很多的观点。不管你持什么观点,完全理解这个问题,并且创建容易理解的设计是很重要的。

7.4.1 本章的新函数

本章涉及的新函数如表7-1所示。

表7-1 本章的新函数

callable(object)                    确定对象是否可调用(比如函数或者方法)
getattr(object, name[ ,default])    确定特性的值,可选择提供默认值
hasattr(object, name)               确定对象是否有给定的特性
isinstance(object, class)           确定对象是否是类的实例
issubclass(A, B)                    确定A是否为B的子类
random.choice(sequence)             从非空序列中随机选择元素
setattr(object, name, value)        设定对象的给定特性为value
type(object)                        返回对象的类型

7.4.2 接下来学什么

前面已经介绍了许多关于创建自己的对象以及自定义对象的作用。在轻率地进军Python特殊方法的魔法阵(第九章)之前,让我们先喘口气,看看介绍异常处理的简短的一章。