购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 面向对象编程

在上一节中,我们讨论了函数式编程和相应的编程模式。现在我们将学习另一个范式:面向对象范式(object-oriented paradigm)。函数式编程对应函数,面向对象编程则对应对象。那么,第一个问题:什么是对象?

对象的定义有好几种。相比于其在面向对象编程理论中的标准学术定义,我想试着给出一个非常规的解释。

从实践的角度来看,我们可以将对象看作是特定领域的专家。我们提出问题,他们提供答案;或者我们可以请求他们做一些事情,他们也会去做。我们的问题或请求可能需要复杂的操作,但专家会隐藏其复杂性,这样我们就不需要担心细节——只关心工作是否完成。

以牙医为例。当你去看牙医的时候,你不需要对牙科有多了解,只需要依靠牙医的专业知识来修复蛀牙。你也可以问一些你的牙齿相关的问题,牙医会用一种你能理解的方式来回答,隐藏这个话题实际的复杂性。在上述例子中,牙医就是一个对象——可以完成牙科相关任务或咨询的对象。

对对象做出请求,需要调用对象的方法(method)。方法是属于对象的函数,可以访问对象的内部数据。对象本身有一些数据,通常对外部不可见,有时候对象会以特性(property)的形式公开这些数据。

注意 :方法是属于类的函数,它是类的定义的一部分。需要在定义它的类的实例上被调用(执行)。相比之下,函数不属于任何类,它可以独立运行。

在Python当中,对象中的任何函数或变量都被称为属性(attribute)。特性和方法都是属性。我们将在本章和本书的其余部分中使用这些等价的术语。

现在让我们动起手来,看看如何在Python中定义和处理对象。

2.2.1 类

类(class)定义了如何构造对象以及它们所具有的特征和知识。有些人喜欢将类比作蓝图,它们都是对对象拥有的信息和功能的一般描述。对象和类相关但是不同。如果类是蓝图,那对象就是完工的建筑。

我们在Python中使用保留关键字class定义类。按照惯例,类名以大写字母开头,每个新单词的开头也都用大写字母(此惯例被称为Pascal case)。让我们创建一个模拟咖啡机的类:

在上述代码中,我们定义了一个表示咖啡机的类。我们可以使用这个类来生成新的咖啡机对象,这个过程称为实例化(instantiation)。实例化一个类,是指创建该类的一个新的对象。通过调用类的名称来实例化,就像它是一个返回实例化对象的函数一样:

现在我们有了machine对象,其功能由CoffeeMachine类定义(它仍然是空的,我们将在后面的部分进行完善)。当类被实例化时,它的__init__函数将被调用。在__init__函数中,我们可以进行一些初始化操作。例如,这里我们添加了一个煮咖啡的数量,并将其设置为零:

注意__coffees_brewed开头的两个下划线。如果你还记得之前关于访问级别的讨论,默认情况下,Python的所有数据都对外部可见。双下划线命名模式用于表示某物是私有的,不希望被直接访问。

在本例中,我们不希望外界访问__coffees_brewed;否则他们可以随意改变咖啡冲泡次数!

如果不能访问__coffees_brewed,那我们如何知道机器煮了多少杯咖啡呢?答案是特性。特性是类的只读属性。不过,在讨论特性之前,还有一些语法需要介绍。

1.变量self

如果查看前面的示例,你会发现我们经常使用一个名为self的变量。我们也可以使用其他名称,不过约定使用self。正如你前面看到的,我们将其传递给类中的每个函数,包括初始化函数。多亏了self这第一个形参,我们才可以访问类中定义的所有数据。例如,在__init__函数中,我们将变量__coffees_brewed加到self后面,这样,这个变量就存在于对象中了。

变量self必须是类中每个函数定义的第一个形参,但是当我们在类的实例上调用这些函数时,它不需要作为第一个实参数被传递。例如,为了实例化CoffeeMachine类,我们写了如下代码:

调用初始化函数时没有任何形参(没有self)。如果你细想一下,如果我们还没有初始化对象,怎么可能将该初始化函数作为self传递呢?原来,Python已经为我们解决了这个问题:我们永远不需要将self传递给初始化函数或对象的任何方法或特性。

调用self正是指类的不同属性访问类中的其他定义的方式。例如,在我们稍后将编写的brew_coffee方法中,我们正是使用self来访问__coffees_brewed的数量:

理解self以后,我们就可以学习特性了。

2.类的特性

对象的特性(property)是可以返回数据的只读属性。使用点号即可访问对象的特性:object.property。还是以咖啡机为例,我们可以添加一个coffees_brewed特性(用咖啡机煮的咖啡数量),代码如下:

然后,我们可以访问它:

特性是使用@property装饰器定义的函数:

特性不能接收参数(self除外),且需要有返回值。不返回任值或接收其他参数的特性在概念上就是错误的:特性应该是我们请求对象提供的只读数据。

我们提到,@property是装饰器的一个例子。Python装饰器允许我们修改特性的行为。@property修改类的函数,以便它可以像类的属性一样被使用。本书不会再使用其他装饰器,所以我们不会对此进行讲解,但如果你有兴趣,我鼓励你自行研究。

特性告诉我们对象的信息。例如,如果我们想知道某个CoffeeMachine类的实例是否至少煮了一杯咖啡,我们可以添加如下特性:

现在就可以询问CoffeeMachine类的实例,是否已经煮过咖啡了:

显然,这台机器还没有准备好咖啡,那么如何让CoffeeMachine的实例为我们煮咖啡呢?使用方法。

3.类的方法

特性允许我们了解对象的某些信息:通过回答我们的询问。为了让对象执行一些任务,我们使用方法。方法(method)不过是属于类的函数,可以访问类中定义的属性。在CoffeeMachine类的代码中,编写一个方法来请求它煮咖啡:

方法将self作为第一个形参,这使它们能够访问类中定义的所有内容。正如我们前面讨论的,在调用对象的方法时,我们不需要传递self参数,Python会为我们代劳。

注意 :特性类似于用@property装饰的方法。特性和方法都将self作为它们的第一个实参。在调用方法时,我们使用括号并可选地传递其参数,但是访问属性时不需要使用括号。

我们可以在实例上调用brew_coffee方法:

既然第一杯咖啡已经煮好,我们可以询问实例:

如你所见,方法必须在类(对象)的特定实例上调用。此对象将是响应请求的对象。函数的调用不需要对象,如下所示:

然而方法必须对对象进行调用,如下所示:

对象只能响应创建它们的类中定义的方法。如果在对象上调用了一个方法(或任何特性),但该方法在类中没有定义,则会触发一个属性错误(AttributeError)。让我们试一试。让咖啡机泡一杯茶,尽管我们从来没有告诉过它怎么泡茶:

好吧,对象“抱怨”说:我们从来没有说过,希望它学会如何泡茶。以下“抱怨”的关键:

教训是:永远不要请求对象做它没有学过的事情,这会吓坏它,并让你的程序失效。

方法可以接受任意数量的形参,但必须在第一个实参self之后定义。例如,让我们在CoffeeMachine类中添加一个方法,让我们能够给咖啡机倒入给定数量的水:

我们可以通过调用这个新方法来给咖啡机实例加水:

在继续学习其他知识之前,关于方法需要知道的最后一点是它们强大的动态调度特性。当在对象上调用方法时,Python将检查该对象是否响应该方法,但是,关键点在于,只要该对象的类定义了所请求的方法,Python并不关心对象的类。

我们可以使用这个特性来定义响应相同方法的不同对象(相同的方法指的是相同的名称和实参),并可以互换地使用它们。例如,我们可以定义一个新的现代咖啡生产商:

现在,我们可以编写函数,期望有一个咖啡生产者(任何定义了brew_coffee()方法的类的对象),并对其执行某些操作:

这个函数对CoffeeMachine和CoffeeHipster的实例都适用:

为了达成这种效果,我们需要确保这些方法具有相同的“签名”,也就是说,它们的名称相同,形参也完全一致。

2.2.2 魔术方法

类可以定义一些特殊方法,称为魔术方法(magic method)或双划方法(双下划线的简写)。这些方法通常不会由我们直接调用,而是由Python在底层使用,我们将在下面的示例中看到。

我们使用过这种方法:__init__,在实例化对象时使用它作为初始化语句。__init__方法定义了创建类的实例时执行的代码。

魔术方法的一个著名用法(本书将会大量使用)是重载运算符。让我们通过一个例子来看一看。假设我们创建了一个表示复数的类:

如何在ComplexNum的实例上实现加法操作?方法一是添加一个plus方法:

可以像下面这样使用:

这是可以的,但是如果能像对数字那样使用+运算符,显然会更好:

Python包含一个魔术方法__add__。如果创建了这个方法,我们就可以使用+运算符,Python将在后台调用__add__方法。因此,如果我们将plus重命名为__add__,就可以使用+运算符对ComplexNum进行加法操作了:

我们可以在类中创建更多的魔术方法,来执行减法、除法、比较等。你可以快速浏览一下表4-1,看一看魔术方法可以实现的操作。例如,创建__sub__方法后就可以使用-运算符简单地计算两个复数的减法:

现在,我们可以使用-运算符:

怎样才能使用==操作符比较实例是否相等呢?创建__eq__方法就行:

这样就可以轻易地比较复数大小了:

本书会使用很多魔术方法,它们确实提高了代码的可读性。

现在,让我们换换话题,学习类型提示。 L8IKALwgb/u1StxM7BWmM6G2lUAjDwziOooa3WqzWdJ/C1aGkxaUv+3GwxwWLUdZ

点击中间区域
呼出菜单
上一章
目录
下一章
×