← 学习库 Think Python (2e) · 中英对照 目录

Chapter 17  Classes and methods 第 17 章 类与方法

本页译自 Think Python 2e(Allen B. Downey)· Chapter 17 Classes and methods。代码块保留英文原文不翻译;正文段段对照,中文块可用右下角按钮隐藏。

Code examples from this chapter are available from http://thinkpython.com/code/Time2.py.

本章的代码示例可从 http://thinkpython.com/code/Time2.py 获取。

17.1 Object-oriented features 17.1 面向对象的特性

Python is an object-oriented programming language, which means that it provides features that support object-oriented programming. It is not easy to define object-oriented programming, but we have already seen some of its characteristics:

Python 是一种面向对象编程语言,也就是说它提供了支撑面向对象编程的特性。面向对象编程并不好定义,不过我们已经见识过它的一些特征:

For example, the Time class defined in Chapter 16 corresponds to the way people record the time of day, and the functions we defined correspond to the kinds of things people do with times. Similarly, the Time0 and Time00008 classes correspond to the mathematical concepts of a point and a rectangle.

举例来说,第 16 章定义的 Time 类,对应人们记录一天时间的方式;我们定义的函数,对应用户对时间做的那些事情。同理,Time0 和 Time00011 类对应点和矩形的数学概念。

So far, we have not taken advantage of the features Python provides to support object-oriented programming. These features are not strictly necessary; most of them provide alternative syntax for things we have already done. But in many cases, the alternative is more concise and more accurately conveys the structure of the program.

到目前为止,我们还没有用到 Python 为支持面向对象编程而提供的特性。这些特性并非必不可少;它们大多只是为我们已经做过的事情提供了另一种语法。但在很多情况下,这种替代写法更简洁,也更贴切地表达了程序的结构。

For example, in the Time program, there is no obvious connection between the class definition and the function definitions that follow. With some examination, it is apparent that every function takes at least one Time object as an argument.

比如,在 Time 程序里,类定义和它后面的函数定义之间没有明显的关联。仔细看一下就会发现,每个函数都至少把一个 Time 对象当作实参。

This observation is the motivation for methods; a method is a function that is associated with a particular class. We have seen methods for strings, lists, dictionaries and tuples. In this chapter, we will define methods for user-defined types.

这一观察正是方法的来由:方法就是一种与某个特定类关联的函数。我们见过字符串、列表、字典和元组的方法。本章中,我们要为用户自定义类型定义方法。

Methods are semantically the same as functions, but there are two syntactic differences:

方法在语义上与函数相同,但有两点语法上的区别:

In the next few sections, we will take the functions from the previous two chapters and transform them into methods. This transformation is purely mechanical; you can do it simply by following a sequence of steps. If you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing.

在接下来几节里,我们要把前 two 章的函数改写成方法。这种改写纯属机械操作,只要按部就班就能完成。如果你对两种形式之间的转换游刃有余,就能为手头的工作选出最合适的写法。

17.2 Printing objects 17.2 打印对象

In Chapter 16, we defined a class named Time and in Exercise 1, you wrote a function named Time00017:

在第 16 章,我们定义了一个名为 Time 的类;在习题 1 中,你写了一个名为 Time00019 的函数:
Time00020

To call this function, you have to pass a Time object as an argument:

要调用这个函数,必须传入一个 Time 对象作为实参:
Time00023

To make Time00024 a method, all we have to do is move the function definition inside the class definition. Notice the change in indentation.

要把 Time00025 变成方法,只需把函数定义搬进类定义内部。注意缩进的变化。
Time00026

Now there are two ways to call Time00027. The first (and less common) way is to use function syntax:

现在有两种方式调用 Time00028。第一种(较少见)是用函数语法:
Time00029

In this use of dot notation, Time is the name of the class, and Time00031 is the name of the method. Time0 is passed as a parameter.

这种点号记法中,Time 是类的名字,Time00034 是方法的名字。Time0 作为形参传入。

The second (and more concise) way is to use method syntax:

第二种(更简洁)是用方法语法:
Time00036

In this use of dot notation, Time00037 is the name of the method (again), and Time0 is the object the method is invoked on, which is called the subject. Just as the subject of a sentence is what the sentence is about, the subject of a method invocation is what the method is about.

这种点号记法中,Time00039 还是方法的名字,而 Time0 是方法所调用的对象,叫做主语(subject)。正如句子的主语是句子所谈论的对象,方法调用的主语就是方法所作用的对象。

Inside the method, the subject is assigned to the first parameter, so in this case Time0 is assigned to Time.

在方法内部,主语被赋给第一个形参,所以此例中 Time0 被赋给了 Time

By convention, the first parameter of a method is called Time, so it would be more common to write Time00046 like this:

按照惯例,方法的第一个形参叫做 Time,所以更常见的写法是这样写 Time00048:
Time00049

The reason for this convention is an implicit metaphor:

采用这个惯例,背后是一种隐性的比喻:

This change in perspective might be more polite, but it is not obvious that it is useful. In the examples we have seen so far, it may not be. But sometimes shifting responsibility from the functions onto the objects makes it possible to write more versatile functions, and makes it easier to maintain and reuse code.

这种视角的转变或许更客气,但是否有用并不明显。就目前见过的例子而言,可能没用。但有时把责任从函数转移到对象身上,反而能写出更通用的函数,也更容易维护和复用代码。

Exercise 1

习题 1

Rewrite Time00058 (from Section 16.4) as a method. It is probably not appropriate to rewrite Time00059 as a method; what object you would invoke it on?

Time00060(第 16.4 节)改写成一个方法。把 Time00061 改写成方法大概并不合适——你该在哪个对象上调用它?

17.3 Another example 17.3 另一个例子

Here's a version of Time00062 (from Section 16.3) rewritten as a method:

下面是把 Time00063(第 16.3 节)改写成方法的版本:
Time00064

This version assumes that Time00065 is written as a method, as in Exercise 1. Also, note that it is a pure function, not a modifier.

这个版本假定 Time00066 已写成方法,如习题 1 那样。另外注意,它是个纯函数,不是修改器。

Here's how you would invoke Time00067:

调用 Time00068 的方式如下:
Time00069

The subject, Time0, gets assigned to the first parameter, Time. The argument, Time, gets assigned to the second parameter, Time000.

主语 Time0 被赋给第一个形参 Time。实参 Time 被赋给第二个形参 Time000。

This mechanism can be confusing, especially if you make an error. For example, if you invoke Time00078 with two arguments, you get:

这个机制容易让人犯迷糊,尤其是出错的时候。比如,如果你用两个实参调用 Time00079,会得到:
Time00080

The error message is initially confusing, because there are only two arguments in parentheses. But the subject is also considered an argument, so all together that's three.

这个报错一开始会让人困惑,因为括号里明明只有两个实参。但主语也算一个实参,所以加起来是三个。

17.4 A more complicated example 17.4 一个更复杂的例子

Time0008 (from Exercise 2) is slightly more complicated because it takes two Time objects as parameters. In this case it is conventional to name the first parameter Time and the second parameter Time0:

Time0008(习题 2)稍微复杂一点,因为它以两个 Time 对象作为形参。这种情况下,习惯上把第一个形参命名为 Time,第二个命名为 Time0:
Time00087

To use this method, you have to invoke it on one object and pass the other as an argument:

要用这个方法,得在一个对象上调用它,并把另一个作为实参传入:
Time00088

One nice thing about this syntax is that it almost reads like English: “end is after start?”

这种语法有个妙处:读起来几乎就像英文句子——“end is after start?”(end 在 start 之后吗?)

17.5 The init method 17.5 Time0008 方法

The init method (short for “initialization”) is a special method that gets invoked when an object is instantiated. Its full name is Time0009 (two underscore characters, followed by Time, and then two more underscores). An init method for the Time class might look like this:

init 方法(“initialization”的缩写)是一种特殊方法,在对象被实例化时被调用。它的全名是 Time0009(两个下划线字符,后面跟 Time,再跟两个下划线)。给 Time 类写的 init 方法大概长这样:
Time00096

It is common for the parameters of Time0009 to have the same names as the attributes. The statement

Time0009 的形参常与属性同名。下面这条语句
Time00099

stores the value of the parameter Time as an attribute of Time.

把形参 Time 的值存为 Time 的一个属性。

The parameters are optional, so if you call Time with no arguments, you get the default values.

这些形参都是可选的,所以如果你不带实参调用 Time,得到的就是默认值。
Time00106

If you provide one argument, it overrides Time:

如果只传一个实参,它覆盖 Time
Time00109

If you provide two arguments, they override Time and Time00.

如果传两个实参,它们覆盖 TimeTime00。
Time00114

And if you provide three arguments, they override all three default values.

如果传三个实参,它们就覆盖全部三个默认值。

Exercise 2

习题 2

Write an init method for the Time0 class that takes x and x as optional parameters and assigns them to the corresponding attributes.

Time0 类写一个 init 方法,以 xx 作为可选形参,并把它们赋给对应的属性。

17.6 The Time001 method 17.6 Time001 方法

Time001 is a special method, like Time0012, that is supposed to return a string representation of an object.

Time001 和 Time0012 一样,是一种特殊方法,作用是返回一个对象的字符串表示。

For example, here is a x00 method for Time objects:

比如,下面是给 Time 对象写的 x00 方法:
Time00129

When you Time0 an object, Python invokes the x00 method:

当你 Time0 一个对象时,Python 会调用它的 x00 方法:
Time00134

When I write a new class, I almost always start by writing Time0013, which makes it easier to instantiate objects, and Time001, which is useful for debugging.

每当我写一个新课,几乎总是先写 Time0013(让对象的实例化更方便)和 Time001(对调试很有用)。

Exercise 3

习题 3

Write a x00 method for the Time0 class. Create a Point object and print it.

Time0 类写一个 x00 方法。创建一个 Point 对象并打印它。

17.7 Operator overloading 17.7 运算符重载

By defining other special methods, you can specify the behavior of operators on user-defined types. For example, if you define a method named Time001 for the Time class, you can use the x operator on Time objects.

通过定义其他特殊方法,你可以规定运算符在用户自定义类型上的行为。比如,如果你给 Time 类定义了一个名为 Time001 的方法,就能对 Time 对象使用 x 运算符。

Here is what the definition might look like:

定义大致如下:
Time00149

And here is how you could use it:

用法如下:
Time00150

When you apply the x operator to Time objects, Python invokes Time001. When you print the result, Python invokes Time001. So there is quite a lot happening behind the scenes!

当你把 x 运算符用在 Time 对象上时,Python 调用 Time001;打印结果时,Python 调用 Time001。所以幕后其实发生了不少事情!

Changing the behavior of an operator so that it works with user-defined types is called operator overloading. For every operator in Python there is a corresponding special method, like Time001. For more details, see Time00158.

改变运算符的行为,让它适用于用户自定义类型,叫做运算符重载。Python 里每个运算符都有一个对应的特殊方法,比如 Time001。更多细节见 Time00160。

Exercise 4

习题 4

Write an x00 method for the Point class.

为 Point 类写一个 x00 方法。

17.8 Type-based dispatch 17.8 按类型分派

In the previous section we added two Time objects, but you also might want to add an integer to a Time object. The following is a version of Time001 that checks the type of Time0 and invokes either Time0016 or Time00166:

上一节我们把两个 Time 对象相加,但你可能也想把一个整数加到 Time 对象上。下面这个 Time001 版本会检查 Time0 的类型,再决定调用 Time0016 还是 Time00170:
Time00171

The built-in function Time00172 takes a value and a class object, and returns Time if the value is an instance of the class.

内置函数 Time00174 接受一个值和一个类对象,如果该值属于这个类,就返回 Time

If Time0 is a Time object, Time001 invokes Time0017. Otherwise it assumes that the parameter is a number and invokes Time00179. This operation is called a type-based dispatch because it dispatches the computation to different methods based on the type of the arguments.

如果 Time0 是 Time 对象,Time001 就调用 Time0018;否则它假定这个实参是个数,调用 Time00183。这种操作叫做按类型分派,因为它根据实参的类型,把计算分派给不同的方法。

Here are examples that use the x operator with different types:

下面是用 x 运算符搭配不同类型的例子:
Time00186

Unfortunately, this implementation of addition is not commutative. If the integer is the first operand, you get

可惜,这种加法实现不满足交换律。如果整数作为第一个操作数,你会得到
Time00187

The problem is, instead of asking the Time object to add an integer, Python is asking an integer to add a Time object, and it doesn't know how to do that. But there is a clever solution for this problem: the special method Time0018, which stands for “right-side add.” This method is invoked when a Time object appears on the right side of the x operator. Here's the definition:

问题在于:Python 没有让 Time 对象去加一个整数,而是让一个整数去加 Time 对象,而它不知道该怎么做。不过有个巧妙的解法:特殊方法 Time0019,意思是“右侧加法(right-side add)”。当 Time 对象出现在 x 运算符右侧时,就会调用这个方法。定义如下:
Time00192

And here's how it's used:

用法如下:
Time00193

Exercise 5

习题 5

Write an x00 method for Points that works with either a Point object or a tuple:

为 Point 写一个 x00 方法,既能与 Point 对象相加,也能与元组相加:

17.9 Polymorphism 17.9 多态

Type-based dispatch is useful when it is necessary, but (fortunately) it is not always necessary. Often you can avoid it by writing functions that work correctly for arguments with different types.

按类型分派在必要时有用,但(幸好)并非总是必要。你常能通过写一些对不同类型实参都正确工作的函数,来避免它。

Many of the functions we wrote for strings will actually work for any kind of sequence. For example, in Section 11.1 we used Time00196 to count the number of times each letter appears in a word.

我们为字符串写的很多函数,其实对任何序列都能用。比如第 11.1 节里,我们用 Time00197 统计一个单词中每个字母出现的次数。
Time00198

This function also works for lists, tuples, and even dictionaries, as long as the elements of x are hashable, so they can be used as keys in x.

这个函数对列表、元组甚至字典也有效,只要 x 的元素是可散列的,从而能当作 x 的键。
Time00203

Functions that can work with several types are called polymorphic. Polymorphism can facilitate code reuse. For example, the built-in function x00, which adds the elements of a sequence, works as long as the elements of the sequence support addition.

能适用于多种类型的函数叫做多态的。多态有助于代码复用。比如内置函数 x00 会把序列里的元素相加,只要这些元素支持加法就能工作。

Since Time objects provide an x00 method, they work with x00:

既然 Time 对象提供了 x00 方法,它们就能配合 x00 使用:
Time00210

In general, if all of the operations inside a function work with a given type, then the function works with that type.

一般规律是:如果函数内部的所有操作都能用于某个类型,那么这个函数就能用于该类型。

The best kind of polymorphism is the unintentional kind, where you discover that a function you already wrote can be applied to a type you never planned for.

最好的多态是那种“无心插柳”的多态——你发现之前写好的函数,竟能用到一个从未打算支持的类型上。

17.10 Debugging 17.10 调试

It is legal to add attributes to objects at any point in the execution of a program, but if you are a stickler for type theory, it is a dubious practice to have objects of the same type with different attribute sets. It is usually a good idea to initialize all of an object's attributes in the init method.

在程序运行的任何时刻给对象添加属性都是合法的,但如果你对类型理论很较真,让同类型的对象拥有不同的属性集合就有点不妥。通常最好在 init 方法里把对象的所有属性都初始化好。

If you are not sure whether an object has a particular attribute, you can use the built-in function Time002 (see Section 15.7).

如果你不确定某个对象是否有某个属性,可以用内置函数 Time002(见第 15.7 节)。

Another way to access the attributes of an object is through the special attribute Time0021, which is a dictionary that maps attribute names (as strings) and values:

访问对象属性的另一种方式,是通过特殊属性 Time0021,它是一个把属性名(字符串)映射到属性值的字典:
Time00215

For purposes of debugging, you might find it useful to keep this function handy:

为了调试,你可能会觉得下面这个函数很顺手:
Time00216

Time00217 traverses the items in the object's dictionary and prints each attribute name and its corresponding value.

Time00218 遍历对象字典里的条目,打印每个属性名及其对应的值。

The built-in function Time002 takes an object and an attribute name (as a string) and returns the attribute's value.

内置函数 Time002 接受一个对象和一个属性名(字符串),返回该属性的值。

17.11 Interface and implementation 17.11 接口与实现

One of the goals of object-oriented design is to make software more maintainable, which means that you can keep the program working when other parts of the system change, and modify the program to meet new requirements.

面向对象设计的目标之一,是让软件更易维护——也就是说当系统的其他部分变化时,程序仍能正常工作,并且能修改程序以满足新需求。

A design principle that helps achieve that goal is to keep interfaces separate from implementations. For objects, that means that the methods a class provides should not depend on how the attributes are represented.

有助于实现这一目标的设计原则,是把接口与实现分开。对于对象来说,这意味着一个类提供的方法,不应依赖于属性是如何表示的。

For example, in this chapter we developed a class that represents a time of day. Methods provided by this class include Time00221, Time0022, and Time0022.

比如,本章我们开发了一个表示一天时间的类。它提供的方法包括 Time00224、Time0022 和 Time0022。

We could implement those methods in several ways. The details of the implementation depend on how we represent time. In this chapter, the attributes of a Time object are Time, Time00, and Time00.

这些方法的实现可以有很多种。实现的细节取决于我们如何表示时间。本章里,Time 对象的属性是 TimeTime00 和 Time00。

As an alternative, we could replace these attributes with a single integer representing the number of seconds since midnight. This implementation would make some methods, like Time0023, easier to write, but it makes some methods harder.

另一种做法是,把这些属性换成一个整数,表示自午夜以来的秒数。这种实现会让某些方法(比如 Time0023)更好写,却也让另一些方法更难写。

After you deploy a new class, you might discover a better implementation. If other parts of the program are using your class, it might be time-consuming and error-prone to change the interface.

一个新类部署之后,你可能会发现更好的实现。如果程序的其他部分正在使用你的类,改动接口既费时又容易出错。

But if you designed the interface carefully, you can change the implementation without changing the interface, which means that other parts of the program don't have to change.

但如果你把接口设计得当,就能在不改接口的前提下更换实现,也就是说程序的其他部分无需改动。

Keeping the interface separate from the implementation means that you have to hide the attributes. Code in other parts of the program (outside the class definition) should use methods to read and modify the state of the object. They should not access the attributes directly. This principle is called information hiding; see Time00237.

把接口与实现分开,意味着你必须隐藏属性。程序其他部分(类定义之外)的代码,应当通过方法来读取和修改对象的状态,而不应直接访问属性。这个原则叫做信息隐藏,见 Time00238。

Exercise 6

习题 6

Download the code from this chapter (Time00239). Change the attributes of Time to be a single integer representing seconds since midnight. Then modify the methods (and the function Time00241) to work with the new implementation. You should not have to modify the test code in Time. When you are done, the output should be the same as before. Solution: Time00243

下载本章代码(Time00244)。把 Time 的属性改成单个整数,表示自午夜以来的秒数。然后修改各方法(以及 Time00246 函数)以配合新实现。你不必修改 Time 里的测试代码。完成后,输出应与之前一致。答案:Time00248

17.12 Glossary 17.12 术语表

object-oriented language:
A language that provides features, such as user-defined classes and method syntax, that facilitate object-oriented programming.
object-oriented programming:
A style of programming in which data and the operations that manipulate it are organized into classes and methods.
method:
A function that is defined inside a class definition and is invoked on instances of that class.
subject:
The object a method is invoked on.
operator overloading:
Changing the behavior of an operator like x so it works with a user-defined type.
type-based dispatch:
A programming pattern that checks the type of an operand and invokes different functions for different types.
polymorphic:
Pertaining to a function that can work with more than one type.
information hiding:
The principle that the interface provided by an object should not depend on its implementation, in particular the representation of its attributes.
面向对象语言(object-oriented language):
提供某些特性(如用户自定义类和方法语法)来支撑面向对象编程的语言。
面向对象编程(object-oriented programming):
一种编程风格,把数据以及操作数据的动作组织成类和方法。
方法(method):
定义在类定义内部、并在该类实例上被调用的函数。
主语(subject):
方法被调用时所作用的那个对象。
运算符重载(operator overloading):
改变像 x 这样的运算符的行为,让它适用于用户自定义类型。
按类型分派(type-based dispatch):
一种编程模式:检查操作数的类型,对不同类型的操作数调用不同的函数。
多态的(polymorphic):
形容一个能适用于多种类型的函数。
信息隐藏(information hiding):
一个原则:对象所提供的接口不应依赖于它的实现,尤其是属性的表示方式。

17.13 Exercises 17.13 习题

Exercise 7

习题 7

This exercise is a cautionary tale about one of the most common, and difficult to find, errors in Python. Write a definition for a class named Time0025 with the following methods:

这个习题是一则警示故事,讲的是 Python 中最常见也最难发现的错误之一。请为一个名为 Time0025 的类写定义,包含以下方法:
  1. An Time0025 method that initializes an attribute named Time00254 to an empty list.
  2. A method named Time00255 that takes an object of any type and adds it to Time00256.
  3. A Time002 method that returns a string representation of the Kangaroo object and the contents of the pouch.
  1. 一个 Time0025 方法,把名为 Time00259 的属性初始化为空列表。
  2. 一个名为 Time00260 的方法,接受一个任意类型的对象,把它加进 Time00261。
  3. 一个 Time002 方法,返回 Kangaroo 对象及其育儿袋内容的字符串表示。

Test your code by creating two Time0026 objects, assigning them to variables named Time0 and x00, and then adding x00 to the contents of Time0's pouch.

创建两个 Time0026 对象来测试你的代码,分别赋给名为 Time0 和 x00 的变量,然后把 x00 加进 Time0 的育儿袋内容里。

Download Time00273. It contains a solution to the previous problem with one big, nasty bug. Find and fix the bug.

下载 Time00274。它包含上一题的一个解法,但藏着一个又大又讨厌的 bug。找出并修复这个 bug。

If you get stuck, you can download Time00275, which explains the problem and demonstrates a solution.

如果卡住了,可以下载 Time00276,里面解释了问题并演示了一种解法。

Exercise 8

习题 8

Visual is a Python module that provides 3-D graphics. It is not always included in a Python installation, so you might have to install it from your software repository or, if it's not there, from Time00277.

Visual 是一个提供三维图形的 Python 模块。它并非总随 Python 一起安装,所以你可能得从软件仓库安装它;如果仓库里没有,则从 Time00278 安装。

The following example creates a 3-D space that is 256 units wide, long and high, and sets the “center” to be the point (128,128,128). Then it draws a blue sphere.

下面的例子创建了一个宽、长、高都为 256 单位的三维空间,并把“中心”设为点 (128,128,128)。然后画了一个蓝色球体。
Time00279

Time0 is an RGB tuple; that is, the elements are Red-Green-Blue levels between 0.0 and 1.0 (see Time00281).

Time0 是一个 RGB 元组;也就是说,元素是介于 0.0 到 1.0 之间的红-绿-蓝分量(见 Time00283)。

If you run this code, you should see a window with a black background and a blue sphere. If you drag the middle button up and down, you can zoom in and out. You can also rotate the scene by dragging the right button, but with only one sphere in the world, it is hard to tell the difference.

运行这段代码,你应该会看到一个黑底窗口里有一个蓝色球体。上下拖动中键可以缩放;拖动右键也能旋转场景,但世界里只有一个球,很难看出差别。

The following loop creates a cube of spheres:

下面的循环创建一团球体组成的立方体:
Time00284
  1. Put this code in a script and make sure it works for you.
  2. Modify the program so that each sphere in the cube has the color that corresponds to its position in RGB space. Notice that the coordinates are in the range 0–255, but the RGB tuples are in the range 0.0–1.0.
  3. Download Time00285 and use the function Time00286 to generate a list of the available colors on your system, their names and RGB values. For each named color draw a sphere in the position that corresponds to its RGB values.
  1. 把这段代码放进一个脚本,确保它能在你机器上运行。
  2. 修改程序,让立方体里每个球体的颜色对应它在 RGB 空间中的位置。注意坐标是 0–255 的范围,而 RGB 元组是 0.0–1.0 的范围。
  3. 下载 Time00287,用其中的 Time00288 函数生成你系统上可用颜色(名字与 RGB 值)的列表。为每个具名颜色画一个球体,位置对应它的 RGB 值。

You can see my solution at Time00289.

你可以在 Time00290 看到我的解法。