ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 第七章 更加抽象 > 来源: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特殊方法的魔法阵(第九章)之前,让我们先喘口气,看看介绍异常处理的简短的一章。