第七章 更加抽象
来源:http://www.cnblogs.com/Marlowes/p/5426233.html
作者:Marlowes
前几章介绍了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
很多函数和运算符都是多态的——你写的绝大多数程序可能都是,即便你并非有意这样。只要使用多态函数和运算符,就会与“多态”发生关联。事实上,唯一能够毁掉多态的就是使用函数显式地检查类型,比如type
、isinstance
以及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
绑定到该对象上。可以使用setName
和getName
方法(假设已经由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
方法再写一次。那么该怎么办?可以让Rectangle
从Shape
类继承方法。在Rectangle
对象上调用draw
方法时,程序会自动从Shape
类调用该方法。(参见7.2.5节)。
7.2 类和类型
现在读者可能对什么是类有了大体感觉——或者已经有些不耐烦听我对它进行更多介绍了。在开始介绍之前,先来认识一下什么是类,以及它和类型又有什么不同(或相同)。
7.2.1 类到底是什么
前面的部分中,类这个词已经多次出现,可以将它或多或少地视为种类或者类型的同义词。从很多方面来说,这就是类——一种对象。所有的对象都属于某一个类,称为类的实例(instance)。
例如,现在请往窗外看,鸟就是“鸟类” 的实例。鸟类是一个非常通用(抽象)的类,具有很多子类:看到的鸟可能属于子类“百灵鸟”。可以将“鸟类”想象成所有鸟的集合,而“百灵鸟类”是其中的一个子集。当一个对象所属的类是另外一个对象所属类的子集时,前者就被成为后者的子类(subclass),所以“百灵鸟类”是“鸟类”的子类。相反,“鸟类”是“百灵鸟类”的超类(superclass)。
注:日常交谈中,可能经常用复数来描述对象的类,比如birds
或者larkes
。Python中,习惯上都使用单数名词,并且首字母大写,比如Bird
和Lark
。
这样一比喻,子类和超类就容易理解了。但是在面向对象程序设计中,子类的关系是隐式的,因为一个类的定义取决于它所支持的方法。类的所有实例都会包含这些方法,所以所有子类的所有实例都有这些方法。定义子类只是个定义更多(也有可能是重载已经存在的)的方法的过程。
例如,鸟类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
的用处了。在调用foo
的setName
和greet
函数时,foo
自动将自己作为第一个参数传入函数中——因此形象的命名为self
。对于这个变量,每个人可能都会有自己的叫法,但是因为它总是对象自身,所以习惯上总是叫做self
。
显然这就是self
的用处和存在的必要性。没有它的话,成员方法就没法访问他们要对其特性进行操作的对象本身了。
和之前一样,特性是可以在外部访问的:
>>> foo.name 'Luke Skywalker'
>>> bar.name = "Yoda"
>>> bar.greet()
Hello, world! I'm Yoda
注:如果知道foo
是Person
的实例的话,那么还可以把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的对象特性只允许由同一个对象的方法访问)觉得这样做就破坏了封装的原则。他们认为对象的状态对于外部应该是完全隐藏(不可访问)的。有人可能会奇怪为什么他们会站在如此极端的立场上。每个对象管理自己的特性还不够吗?为什么还要对外部世界隐藏呢?毕竟如果能直接使用ClosedObject
的name
特性的话就不用使用setName
和getName
方法了。
关键在于其他程序员可能不知道(可能也不应该知道)你的对象内部的具体操作。例如,ClosedObject
可能会在其他对象更改自己的名字的时候,给一些管理员发送邮件消息。这应该是setName
方法的一部分。但是如果直接使用c.name
设定名字会发生什么?什么都没发生,Email也没发出去。为了避免这类事情的发生,应该使用私有(private)特性,这是外部对象无法访问,但getName
和setName
等访问器(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
定义的两个要点。
☑ 这里用提供新定义的方式重写了Filter
的init
定义。
☑ 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
类的间接实例,因为SPAMFilter
是Filter
的子类。另外一种说法就是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
的方法,那么它就会重写Talker
的talk
方法(使其不可访问)。如果把它们的顺序调过来,像下面这样:
class TalkingCalculator(Talker, Calculator):
pass
就会让Talker
的talk
方法可用了。如果超类们共享一个超类,那么在查找给定方法或者属性时访问超的顺序称为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特殊方法的魔法阵(第九章)之前,让我们先喘口气,看看介绍异常处理的简短的一章。