13.1 引言

在摸清OOP和类的本质之前,我们首先讲一些高级主题,然后通过几个简单的例子热一热身。如果你刚学习面向对象编程,你可以先跳过这部分内容,直接进入第13. 2节。如果你对有关面向对象编程已经熟悉了,并且想了解它在Python中是怎样表现的,那么先看一下这部分内容,然后再进入第13. 3节,一探究竟!

在Python中,面向对象编程主要有两个主题,就是类和类实例(见图13-1)。

13.1 引言 - 图1

图  13-1

1. 类与实例

类与实例相互关联着:类是对象的定义,而实例是“真正的实物”,它存放了类中所定义的对象的具体信息。

下面的示例展示了如何创建一个类:

13.1 引言 - 图2

关键字是class,紧接着是一个类名。随后是定义类的类体代码。这里通常由各种各样的定义和声明组成。新式类和经典类声明的最大不同在于,所有新式类必须继承至少一个父类,参数bases可以是一个(单继承)或多个(多重继承)用于继承的父类。

object是“所有类之母”。如果你的类没有继承任何其他父类,object将作为默认的父类。它位于所有类继承结构的最上层。如果你没有直接或间接的子类化一个对象,那么你就定义了一个经典类:

13.1 引言 - 图3

如果你没有指定一个父类,或者如果所子类化的基本类没有父类,你这样就是创建了一个经典类。很多Python类都还是经典类。即使经典类已经过时了,在以后的Python版本中,仍然可以使用它们。不过我们强烈推荐你尽可能使用新式类,尽管对于学习来说,两者都行。

左边的工厂制造机器相当于类,而生产出来的玩具就是它们各个类的实例。尽管每个实例都有一个基本的结构,但各自的属性像颜色或尺寸可以改变——这就好比实例的属性。

创建一个实例的过程称作实例化,过程如下(注意:没有使用new关键字):

13.1 引言 - 图4

类名使用我们所熟悉的函数操作符(()),以“函数调用”的形式出现。然后你通常会把这个新建的实例赋给一个变量。赋值在语法上不是必须的,但如果你没有把这个实例保存到一个变量中,它就没用了,会被自动垃圾收集器回收,因为任何引用指向这个实例。这样,你刚刚所做的一切,就是为那个实例分配了一块内存,随即又释放了它。

类既可以很简单,也可以很复杂,这全凭你的需要。最简单的情况,类仅用作名称空间(namespace)(参见第11章)。这意味着你把数据保存在变量中,对他们按名称空间进行分组,使得他们处于同样的关系空间中——所谓的关系是使用标准Python句点属性标识。例如,你有一个本身没有任何属性的类,使用它仅对数据提供一个名字空间,让你的类拥有像Pascal中的记录集(record)和C语言中的结构体(structure)一样的特性,或者换句话说,这样的类仅作为容器对象来共享名字空间。

示例如下:

13.1 引言 - 图5

注意有的地方在语法构成上需要有一行语句,但实际上不需要做任何操作,这时候可以使用pass语句。这种情况,必要的代码就是类体,但我们暂不想提供这些。上面定义的类没有任何方法或属性。下面我们创建一个实例,它只使用类作为名称空间容器。

13.1 引言 - 图6

13.1 引言 - 图7

我们当然也可以使用变量“x”,“y”来完成同样的事情,但本例中,实例名字mathObj将mathObj.x和mathObj.y关联起来。这就是我们所说的使用类作为名字空间容器。mathObj.x和mathObj.y是实例属性,因为它们不是类MyData的属性,而是实例对象(mathObj)的独有属性。本章后面,我们将看到这些属性实质上是动态的:你不需要在构造器中,或其他任何地方为它们预先声明或者赋值。

2. 方法

我们改进类的方式之一就是给类添加功能。类的功能有一个更通俗的名字叫方法。在Python中,方法定义在类定义中,但只能被实例所调用。也就是说,调用一个方法的最终途径必须是这样的:(1)定义类(和方法);(2)创建一个实例;(3)最后一步,用这个实例调用方法。例如:

13.1 引言 - 图8

你可能注意到了self参数,它在所有的方法声明中都存在。这个参数代表实例对象本身,当你用实例调用方法时,由解释器悄悄地传递给方法的,所以,你不需要自己传递self进来,因为它是自动传入的。

举例说明一下,假如你有一个带两参数的方法,所有你的调用只需要传递第二个参数,Python把self作为第一个参数传递进来,如果你犯错的话,也不要紧。Python将告诉你传入的参数个数有误。总之,你只会犯一次错,下一次……你当然就记得了!

这种需要在每个方法中给出实例(self)的要求对于那些使用C++或Java的人可能是一种新的体验,所以请意识到这点。

Python的哲学本质上就是要明白清晰。在其他语言中,self称为“this”。可以在13. 7—节的”核心笔记”中找到有关self更多内容。一般的方法会需要这个实例(self),而静态方法或类方法不会,其中类方法需要类而不是实例。在第13. 8节中可以看到有关静态方法和类方法的更多内容。

现在我们来实例化这个类,然后调用那个方法:

13.1 引言 - 图9

在本节结束时,我们用一个稍复杂的例子来总结一下这部分内容,这个例子给出如何处理类(和实例),还介绍了一个特殊的方法init(),子类化及继承。

对于已熟悉面向对象编程的人来说,init()类似于类构造器。如果你初涉OOP世界,可以认为一个构造器仅是一个特殊的方法,它在创建一个新的对象时被调用。在Python中,init()实际上不是一个构造器。你没有调用”new”来创建一个新对象。(Python根本就没有“new”关键字)。取而代之,Python创建实例后,在实例化过程中,调用init()方法,当一个类被实例化时,就可以定义额外的行为,比如,设定初始值或者运行一些初步诊断代码——主要是在实例被创建后,实例化调用返回这个实例之前,去执行某些特定的任务或设置。

我们将把print语句添加到方法中,这样我们就清楚什么时候方法被调用了。通常,我们不把输入或输出语句放入函数中,除非预期代码体具有输出的特性。

3. 创建一个类(类定义)

13.1 引言 - 图10

13.1 引言 - 图11

在AddrBookEntry类的定义中,定义了两个方法:init()和updatePhone()init()在实例化时被调用,即,在AddrBookEntry()被调用时。你可以认为实例化是对init()的一种隐式的调用,因为传给AddrBookEntry()的参数完全与init()接收到的参数是一样的(除了self,它是自动传递的)。

回忆一下,当方法在实例中被调用时,self(实例对象)参数自动由解释器传递,所以在上面的init()中,需要的参数是nm和ph,它们分别表示名字和电话号码。init()在实例化时,设置这两个属性,以便在实例从实例化调用中返回时,这两个属性对程序员是可见的。你可能已猜到,updatePhone()方法的目的是替换地址本条目的电话号码属性。

4. 创建实例(实例化)

13.1 引言 - 图12

这就是实例化调用,它会自动调用init()。self把实例对象自动传入)init()。你可以在脑子里把方法中的self用实例名替换掉。在上面第一个例子中,当对象john被实例化后,它的john. name就被设置了,你可在下面得到证实。

另外,如果不存在默认的参数,那么传给init()的两个参数在实例化时是必须的。

5. 访问实例属性

13.1 引言 - 图13

一旦实例被创建后,就可以证实一下,在实例化过程中,我们的实例属性是否确实被init()设置了。我们可以通过解释器“转储”实例来查看它是什么类型的对象。(以后我们将学到如何定制类来获得想要的Python对象字符串的输出形式,而不是现在看到的默认的Python对象字符串(<…>))

6. 方法调用(通过实例)

13.1 引言 - 图14

updatePhone()方法需要一个参数(不计self在内):新的电话号码。在updatePhone()之后,立即检查实例属性,可以证实已生效。

7. 创建子类

靠继承来进行子类化是创建和定制新类类型的一种方式,新的类将保持已存在类所有的特性,而不会改动原来类的定义(指对新类的改动不会影响到原来的类——译者注)。对于新类类型而言,这个新的子类可以定制只属于它的特定功能。除了与父类或基类的关系外,子类与通常的类没有什么区别,也像一般类一样进行实例化。注意下面,子类声明中提到了父类:

13.1 引言 - 图15

现在我们创建了第一个子类,EmplAddrBookEntry。 Python中,当一个类被派生出来,子类就继承了基类的属性,所以,在上面的类中,我们不仅定义了init(), updatEmail()方法,而且EmplAddrBookEntry还从AddrBookEntry中继承了updatePhone()方法。

如果需要,每个子类最好定义它自己的构造器,不然,基类的构造器会被调用。然而,如果子类重写基类的构造器,基类的构造器就不会被自动调用了——这样,基类的构造器就必须显式写出才会被执行,像我们上面那样,用AddrBookEntry.init()设置名字和电话号码。我们的子类在构造器后面几行还设置了另外两个实例属性:员工ID和电子邮件地址。

注意,这里我们要显式传递self实例对象给基类构造器,因为我们不是在该实例中而是在一个子类实例中调用那个方法。因为我们不是通过实例来调用它,这种未绑定的方法调用需要传递一个适当的实例(self)给方法。

本小节后面的例子,告诉我们如何创建子类的实例,访问它们的属性及调用它的方法,包括从父类继承而来的方法。

8. 使用子类

13.1 引言 - 图16

13.1 引言 - 图17

13.1 引言 - 图18核心笔记:命名类、属性和方法

类名通常由大写字母打头。这是标准惯例,可以帮助你识别类,特别是在实例化过程中(有时看起来像函数调用)。还有,数据属性听起来应当是数据值的名字,方法名应当指出对应对象或值的行为。另一种表达方式是:数据值应该使用名词作为名字,方法使用谓词(动词加对象)。数据项是操作的对象、方法应当表明程序员想要在对象进行什么操作。在上面我们定义的类中,遵循了这样的方针,数据值像“name”、“phone”和“email”,行为如“updatePhone”, “updateEmail”。这就是常说的“混合记法(mixedCase)”或“骆驼记法(camelCase)” (因为单词之间没有空格但首字母都大写,这样看上去类似驼峰,故而得名。译者注)。 “Python Style Guide”推荐使用骆驼记法的下划线方式,比如,“update_phone”, “update_email”。类也要细致命名,像“AddrBookEntry”、“RepairShop”等就是很好的名字。

我希望你已初步理解如何在Python中进行面向对象编程了。本章其他小节将带你深入到面向对象编程,Python类及实例的方方面面。