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的实例都适用:

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