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

Chapter 16  Classes and functions 第 16 章 类与函数

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

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

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

16.1 Time 16.1 时间

As another example of a user-defined type, we’ll define a class called Time that records the time of day. The class definition looks like this:

作为用户自定义类型的另一个例子,我们将定义一个名为 Time 的类,用来记录一天中的时间。类的定义如下:
Time00008

We can create a new Time object and assign attributes for hours, minutes, and seconds:

我们可以新建一个 Time 对象,并为时、分、秒赋上属性:
Time00011

The state diagram for the Time object looks like Figure 16.1.

Time 对象的状态图见图 16.1。

Exercise 1

习题 1

Write a function called Time00014 that takes a Time object and prints it in the form Time00015. Hint: the format sequence Time00 prints an integer using at least two digits, including a leading zero if necessary.

编写一个名为 Time00017 的函数,它接受一个 Time 对象,并以 Time00019 的形式打印它。提示:格式序列 Time00 会打印一个整数,至少用两位数字,必要时前面补零。

Exercise 2

习题 2

Write a boolean function called Time0002 that takes two Time objects, t1 and t1, and returns Time if t1 follows t1 chronologically and Time0 otherwise. Challenge: don’t use an t1 statement.

编写一个布尔函数 Time0002,它接受两个 Time 对象 t1t1,如果 t1 在时间上晚于 t1 则返回 Time,否则返回 Time0。挑战:不要使用 t1 语句。

Figure 16.1: Object diagram.

图 16.1:对象图。(原书插图未收录)

16.2 Pure functions 16.2 纯函数

In the next few sections, we’ll write two functions that add time values. They demonstrate two kinds of functions: pure functions and modifiers. They also demonstrate a development plan I’ll call prototype and patch, which is a way of tackling a complex problem by starting with a simple prototype and incrementally dealing with the complications.

在接下来的几节里,我们会编写两个把时间相加的函数。它们展示了两类函数:纯函数和修改器。它们还演示了一种我称之为「原型与打补丁」的开发方案——先从一个简单的原型入手,再逐步处理各种复杂情况,以此应对复杂问题。

Here is a simple prototype of Time0003:

下面是 Time0003 的一个简单原型:
Time00040

The function creates a new Time object, initializes its attributes, and returns a reference to the new object. This is called a pure function because it does not modify any of the objects passed to it as arguments and it has no effect, like displaying a value or getting user input, other than returning a value.

这个函数新建一个 Time 对象,初始化其属性,并返回指向新对象的引用。这称为「纯函数」,因为它不会修改任何作为实参传入的对象,而且除了返回一个值之外,没有任何副作用,比如显示某个值或获取用户输入。

To test this function, I’ll create two Time objects: Time0 contains the start time of a movie, like Monty Python and the Holy Grail, and Time0004 contains the run time of the movie, which is one hour 35 minutes.

为了测试这个函数,我会创建两个 Time 对象:Time0 存一部电影的起始时间,比如《蒙提·派森与圣杯》,Time0004 存电影的片长,即 1 小时 35 分钟。

Time0004 figures out when the movie will be done.

Time0004 算出了电影何时结束。
Time00050

The result, Time0005 might not be what you were hoping for. The problem is that this function does not deal with cases where the number of seconds or minutes adds up to more than sixty. When that happens, we have to “carry” the extra seconds into the minute column or the extra minutes into the hour column.

结果 Time0005 大概不是你想要的。问题在于,当秒数或分钟数相加超过六十时,这个函数并没有处理。发生这种情况时,我们得把多出来的秒「进」到分钟那一列,或者把多出来的分钟「进」到小时那一列。

Here’s an improved version:

下面是一个改进版:
Time00053

Although this function is correct, it is starting to get big. We will see a shorter alternative later.

这个函数虽然正确,但已经开始变长了。稍后我们会看到一个更短的替代写法。

16.3 Modifiers 16.3 修改器

Sometimes it is useful for a function to modify the objects it gets as parameters. In that case, the changes are visible to the caller. Functions that work this way are called modifiers.

有时让函数去修改它作为形参得到的对象很有用。这种情况下,改动对调用者可见。按这种方式工作的函数称为「修改器」。

Time00054, which adds a given number of seconds to a Time object, can be written naturally as a modifier. Here is a rough draft:

Time00056 会给一个 Time 对象加上给定的秒数,它很自然地可以写成一个修改器。下面是一个粗略的草稿:
Time00058

The first line performs the basic operation; the remainder deals with the special cases we saw before.

第一行执行基本操作;其余部分处理我们前面见过的特殊情况。

Is this function correct? What happens if the parameter Time000 is much greater than sixty?

这个函数正确吗?如果形参 Time000 远大于六十,会发生什么?

In that case, it is not enough to carry once; we have to keep doing it until Time00061 is less than sixty. One solution is to replace the t1 statements with Time0 statements. That would make the function correct, but not very efficient.

那种情况下,进位一次并不够;我们得一遍遍地进位,直到 Time00064 小于六十。一个办法是把 t1 语句换成 Time0 语句。这样函数就正确了,但效率并不高。

Exercise 3

习题 3

Write a correct version of Time00067 that doesn’t contain any loops.

写一个正确版本的 Time00068,其中不能包含任何循环。

Anything that can be done with modifiers can also be done with pure functions. In fact, some programming languages only allow pure functions. There is some evidence that programs that use pure functions are faster to develop and less error-prone than programs that use modifiers. But modifiers are convenient at times, and functional programs tend to be less efficient.

凡是用修改器能做的事,用纯函数也都能做。事实上,有些编程语言只允许纯函数。有证据表明,使用纯函数的程序比使用修改器的程序开发更快、更少出错。但修改器有时很方便,而且函数式程序往往效率较低。

In general, I recommend that you write pure functions whenever it is reasonable and resort to modifiers only if there is a compelling advantage. This approach might be called a functional programming style.

一般来说,我建议只要合理就写纯函数,只有在确有显著好处时才诉诸修改器。这种方式可以称为「函数式编程风格」。

Exercise 4

习题 4

Write a “pure” version of Time00069 that creates and returns a new Time object rather than modifying the parameter.

写一个「纯函数」版的 Time00070,它新建并返回一个 Time 对象,而不是修改传入的形参。

16.4 Prototyping versus planning 16.4 原型开发与规划

The development plan I am demonstrating is called “prototype and patch.” For each function, I wrote a prototype that performed the basic calculation and then tested it, patching errors along the way.

我正在演示的开发方案叫做「原型与打补丁」。对每个函数,我先写一个执行基本计算的原型,然后测试它,途中一路修补错误。

This approach can be effective, especially if you don’t yet have a deep understanding of the problem. But incremental corrections can generate code that is unnecessarily complicated—since it deals with many special cases—and unreliable—since it is hard to know if you have found all the errors.

这种方式可能很有效,尤其当你对问题还缺乏深入理解时。但渐进式修补会产生不必要的复杂代码——因为它要处理许多特殊情况——而且不可靠——因为你很难确定是否已经找出了所有错误。

An alternative is planned development, in which high-level insight into the problem can make the programming much easier. In this case, the insight is that a Time object is really a three-digit number in base 60 (see Time00072.)! The Time00 attribute is the “ones column,” the Time00 attribute is the “sixties column,” and the Time attribute is the “thirty-six hundreds column.”

另一种做法是「规划式开发」,对问题的高层洞察能让编程轻松许多。这里的洞察是:一个 Time 对象其实就是以 60 为基数的三位数(参见 Time00077)!Time00 属性是「个位」列,Time00 属性是「六十」列,而 Time 属性是「三千六百(六十的平方)」列。

When we wrote Time0008 and Time00082, we were effectively doing addition in base 60, which is why we had to carry from one column to the next.

我们写 Time0008 和 Time00084 时,实际上是在做以 60 为基数的加法,这正是我们必须从一列向另一列进位的原因。

This observation suggests another approach to the whole problem—we can convert Time objects to integers and take advantage of the fact that the computer knows how to do integer arithmetic.

这一观察提示了处理整个问题的另一种思路——我们可以把 Time 对象转换成整数,并借助计算机本身就会做整数运算这一事实。

Here is a function that converts Times to integers:

下面是一个把 Time 转换成整数的函数:
Time00087

And here is the function that converts integers to Times (recall that Time00 divides the first argument by the second and returns the quotient and remainder as a tuple).

下面是把整数转换成 Time 的函数(回想一下,Time00 把第一个实参除以第二个,并以元组形式返回商和余数)。
Time00091

You might have to think a bit, and run some tests, to convince yourself that these functions are correct. One way to test them is to check that Time00092 for many values of x. This is an example of a consistency check.

你也许得想一想,再跑几个测试,才能让自己相信这些函数是对的。测试它们的一个办法是:对许多不同的 x 值,检查 Time00095。这是一个一致性检查的例子。

Once you are convinced they are correct, you can use them to rewrite Time0009:

一旦你确信它们正确,就可以借助它们重写 Time0009:
Time00098

This version is shorter than the original, and easier to verify.

这个版本比原来的更短,也更容易验证。

Exercise 5

习题 5

Rewrite Time00099 using Time00100 and Time00101.

Time00102 和 Time00103 重写 Time00104。

In some ways, converting from base 60 to base 10 and back is harder than just dealing with times. Base conversion is more abstract; our intuition for dealing with time values is better.

在某些方面,从 60 进制转到 10 进制再转回来,比直接处理时间还要难。进制转换更抽象;而我们处理时间值的直觉更好。

But if we have the insight to treat times as base 60 numbers and make the investment of writing the conversion functions (Time00105 and Time00106), we get a program that is shorter, easier to read and debug, and more reliable.

但如果我们能想到把时间当作 60 进制数字,并舍得花功夫写出转换函数(Time00107 和 Time00108),就能得到一个更短、更易读易调试、也更可靠的程序。

It is also easier to add features later. For example, imagine subtracting two Times to find the duration between them. The naive approach would be to implement subtraction with borrowing. Using the conversion functions would be easier and more likely to be correct.

以后添加功能也更容易。例如,设想把两个 Time 相减来求它们之间的时长。朴素的做法是用借位实现减法;而用转换函数会更简单,也更可能正确。

Ironically, sometimes making a problem harder (or more general) makes it easier (because there are fewer special cases and fewer opportunities for error).

讽刺的是,有时把问题弄得更难(或更一般化),反而让它变得更容易(因为特殊情况更少,出错机会也更少)。

16.5 Debugging 16.5 调试

A Time object is well-formed if the values of Time00 and Time00 are between 0 and 60 (including 0 but not 60) and if Time is positive. Time and Time00 should be integral values, but we might allow Time00 to have a fraction part.

如果一个 Time 对象中,Time00 和 Time00 的值在 0 到 60 之间(含 0 但不含 60),且 Time 为正数,那它就是「格式良好」的。TimeTime00 应为整数值,但我们也许允许 Time00 带小数部分。

Requirements like these are called invariants because they should always be true. To put it a different way, if they are not true, then something has gone wrong.

像这样的要求被称为「不变量」,因为它们应当始终为真。换句话说,如果它们不为真,就说明哪里出了问题。

Writing code to check your invariants can help you detect errors and find their causes. For example, you might have a function like Time00123 that takes a Time object and returns Time0 if it violates an invariant:

写代码来检查你的不变量,有助于发现错误并找出其原因。例如,你可能会有一个像 Time00125 这样的函数,它接受一个 Time 对象,若违反了某个不变量就返回 Time0:
Time00128

Then at the beginning of each function you could check the arguments to make sure they are valid:

然后你可以在每个函数的开头检查实参,确认它们是合法的:
Time00129

Or you could use an Time00 statement, which checks a given invariant and raises an exception if it fails:

或者你也可以用一个 Time00 语句,它会检查给定的不变量,一旦不满足就抛出异常:
Time00132

Time00 statements are useful because they distinguish code that deals with normal conditions from code that checks for errors.

Time00 语句很有用,因为它把处理正常情况的代码和检查错误的代码区分开来。

16.6 Glossary 16.6 术语表

prototype and patch:
A development plan that involves writing a rough draft of a program, testing, and correcting errors as they are found.
planned development:
A development plan that involves high-level insight into the problem and more planning than incremental development or prototype development.
pure function:
A function that does not modify any of the objects it receives as arguments. Most pure functions are fruitful.
modifier:
A function that changes one or more of the objects it receives as arguments. Most modifiers are fruitless.
functional programming style:
A style of program design in which the majority of functions are pure.
invariant:
A condition that should always be true during the execution of a program.
prototype and patch 原型与打补丁:
一种开发方案:先写出程序的粗略草稿,进行测试,并在发现错误时逐一修正。
planned development 规划式开发:
一种开发方案:基于问题的高层洞察进行规划,比渐进式开发或原型开发更有计划性。
pure function 纯函数:
不修改任何作为实参传入的对象的t1。大多数纯函数都是有返回值的。
modifier 修改器:
会修改一个或多个作为实参传入的对象的t1。大多数修改器都没有返回值。
functional programming style 函数式编程风格:
一种程序设计风格,其中绝大多数t1都是纯函数。
invariant 不变量:
在程序执行过程中应当始终为真的条件。

16.7 Exercises 16.7 习题

Code examples from this chapter are available from Time00138; solutions to these exercises are available from Time00139.

本章的代码示例可从 Time00140 获取;这些习题的解答可从 Time00141 获取。

Exercise 6

习题 6

Write a function called Time0014 that takes a Time object and a number and returns a new Time object that contains the product of the original Time and the number.

编写一个名为 Time0014 的函数,它接受一个 Time 对象和一个数字,返回一个新的 Time 对象,其中包含原 Time 与该数字的乘积。

Then use Time0014 to write a function that takes a Time object that represents the finishing time in a race, and a number that represents the distance, and returns a Time object that represents the average pace (time per mile).

然后利用 Time0014 写一个函数:它接受一个表示比赛中完赛时间的 Time 对象,以及一个表示距离的数字,返回一个表示平均配速(每英里用时)的 Time 对象。

Exercise 7

习题 7

The Time0015 module provides Time and Time objects that are similar to the Date and Time objects in this chapter, but they provide a rich set of methods and operators. Read the documentation at Time00154.

Time0015 模块提供了 TimeTime 对象,与本章的 Date 和 Time 对象相似,但它们提供了丰富的方法和运算符。请阅读文档 Time00158。
  1. Use the Time0015 module to write a program that gets the current date and prints the day of the week.
  2. Write a program that takes a birthday as input and prints the user’s age and the number of days, hours, minutes and seconds until their next birthday.
  3. For two people born on different days, there is a day when one is twice as old as the other. That’s their Double Day. Write a program that takes two birthdays and computes their Double Day.
  4. For a little more challenge, write the more general version that computes the day when one person is n times older than the other.
  1. 使用 Time0016 模块写一个程序,获取当前日期并打印星期几。
  2. 写一个程序,输入一个生日,打印用户的年龄,以及距离下一个生日还有多少天、小时、分钟和秒。
  3. 对于两个不同日子出生的人,存在一天,其中一人的年龄恰好是另一人的两倍。那就是他们的「双日」(Double Day)。写一个程序,输入两个生日,算出他们的双日。
  4. 再多一点挑战:写出更通用的版本,算出其中一人年龄是另一人 n 倍的那一天。