Chapter 4 Case study: interface design 第 4 章 案例研究:接口设计
本页译自 Think Python 2e(Allen B. Downey)· Chapter 4 Case study: interface design。代码块保留英文原文不翻译;正文段段对照,中文块可用右下角按钮隐藏。
Code examples from this chapter are available from http://thinkpython.com/code/polygon.py.
http://thinkpython.com/code/polygon.py 获取。4.1 TurtleWorld 4.1 TurtleWorld(海龟世界)
To accompany this book, I have written a package called Swampy. You can download Swampy from http://thinkpython.com/swampy; follow the instructions there to install Swampy on your system.
http://thinkpython.com/swampy 下载它,按那里的说明把 Swampy 装到自己的系统上。A package is a collection of modules; one of the modules in Swampy is TurtleWorld, which provides a set of functions for drawing lines by steering turtles around the screen.
TurtleWorld,它提供了一套函数,让你通过驱赶海龟在屏幕上走动来画线。If Swampy is installed as a package on your system, you can import TurtleWorld like this:
TurtleWorld:from swampy.TurtleWorld import *
If you downloaded the Swampy modules but did not install them as a package, you can either work in the directory that contains the Swampy files, or add that directory to Python’s search path. Then you can import TurtleWorld like this:
TurtleWorld:from TurtleWorld import *
The details of the installation process and setting Python’s search path depend on your system, so rather than include those details here, I will try to maintain current information for several systems at http://thinkpython.com/swampy
http://thinkpython.com/swampy 上维护几种常见系统的最新说明。Create a file named mypolygon.py and type in the following code:
mypolygon.py,输入下面的代码:from swampy.TurtleWorld import *
world = TurtleWorld()
bob = Turtle()
print bob
wait_for_user()
The first line imports everything from the TurtleWorld module in the swampy package.
swampy 包中 swampy024 模块里的所有东西都导入进来。The next lines create a TurtleWorld assigned to world and a Turtle assigned to bob. Printing bob yields something like:
world,一个 Turtle 赋给 bob。打印 bob 会得到类似这样的结果:swampy031
This means that bob refers to an instance of a Turtle as defined in module swampy033 . In this context, “instance” means a member of a set; this Turtle is one of the set of possible Turtles.
bob 指向一个 Turtle 的实例(instance),Turtle 这个类型是在 swampy035 模块里定义的。此处「实例」的意思是某个集合中的一员:这只海龟是所有可能的海龟中的一只。swampy036 tells TurtleWorld to wait for the user to do something, although in this case there’s not much for the user to do except close the window.
swampy037 让 TurtleWorld 等着用户做点什么——不过眼下用户除了关掉窗口也没别的可做。TurtleWorld provides several turtle-steering functions: fd and fd for forward and backward, and fd and fd for left and right turns. Also, each Turtle is holding a pen, which is either down or up; if the pen is down, the Turtle leaves a trail when it moves. The functions fd and fd stand for “pen up” and “pen down.”
fd 和 fd 表示前进和后退,fd 和 fd 表示左转和右转。另外每只海龟都握着一支笔,笔的状态或落下或抬起;笔落下时,海龟移动就会留下痕迹。函数 fd 和 fd 分别代表「抬笔」(pen up)和「落笔」(pen down)。To draw a right angle, add these lines to the program (after creating bob and before calling swampy051 ):
bob 之后、调用 swampy053 之前):swampy054
The first line tells bob to take 100 steps forward. The second line tells him to turn left.
bob 向前走 100 步,第二行让它左转。When you run this program, you should see bob move east and then north, leaving two line segments behind.
bob 先向东走,再向北走,身后留下两段线段。Now modify the program to draw a square. Don’t go on until you’ve got it working!
4.2 Simple repetition 4.2 简单的重复
Chances are you wrote something like this (leaving out the code that creates TurtleWorld and waits for the user):
swampy059
We can do the same thing more concisely with a bob statement. Add this example to swampy061 and run it again:
bob 语句能把同样的事情写得更简洁。把下面这个例子加到 swampy063 里再运行一遍:swampy064
You should see something like this:
swampy065
This is the simplest use of the bob statement; we will see more later. But that should be enough to let you rewrite your square-drawing program. Don’t go on until you do.
bob 语句最简单的用法,后面还会见到更多。但眼下这点知识足够让你重写画正方形的程序了。改完再往下看。Here is a bob statement that draws a square:
bob 语句能画出一个正方形:swampy070
The syntax of a bob statement is similar to a function definition. It has a header that ends with a colon and an indented body. The body can contain any number of statements.
bob 语句的语法和函数定义很像:有一个以冒号结尾的头部,还有一段缩进的主体。主体里可以放任意多条语句。A bob statement is sometimes called a loop because the flow of execution runs through the body and then loops back to the top. In this case, it runs the body four times.
bob 语句有时被称作循环(loop),因为执行流走完主体后又绕回顶部。在这个例子里,主体执行了四次。This version is actually a little different from the previous square-drawing code because it makes another turn after drawing the last side of the square. The extra turn takes a little more time, but it simplifies the code if we do the same thing every time through the loop. This version also has the effect of leaving the turtle back in the starting position, facing in the starting direction.
4.3 Exercises 4.3 习题
The following is a series of exercises using TurtleWorld. They are meant to be fun, but they have a point, too. While you are working on them, think about what the point is.
The following sections have solutions to the exercises, so don’t look until you have finished (or at least tried).
- Write a function called
swampythat takes a parameter namedt, which is a turtle. It should use the turtle to draw a square. Write a function call that passesbobas an argument toswampy, and then run the program again. - Add another parameter, named
swampy, toswampy. Modify the body so length of the sides isswampy, and then modify the function call to provide a second argument. Run the program again. Test your program with a range of values forswampy. - The functions
fdandfdmake 90-degree turns by default, but you can provide a second argument that specifies the number of degrees. For example,swampy085 turnsbob45 degrees to the left. Make a copy ofswampyand change the name toswampy0. Add another parameter namedtand modify the body so it draws an n-sided regular polygon. Hint: The exterior angles of an n-sided regular polygon are 360/n degrees. - Write a function called
swampythat takes a turtle,t, and radius,t, as parameters and that draws an approximate circle by invokingswampy0 with an appropriate length and number of sides. Test your function with a range of values oft. Hint: figure out the circumference of the circle and make sure thatswampy095 . Another hint: ifbobis too slow for you, you can speed him up by changingswampy097, which is the time between moves, in seconds.swampy098 ought to get him moving. - Make a more general version of
swampycalledbobthat takes an additional parameterworld, which determines what fraction of a circle to draw.worldis in units of degrees, so whenswampy103,bobshould draw a complete circle.
- 写一个名为
swampy的函数,它接受一个形参t,即一只海龟。函数应当用这只海龟画出一个正方形。 再写一个函数调用,把bob作为实参传给swampy,然后重新运行程序。 - 给
swampy再加一个形参swampy。修改函数体,让边长等于swampy,再修改函数调用,补上第二个实参。重新运行程序,用一系列不同的swampy取值测试。 - 函数
fd和fd默认转 90 度,但你可以给第二个实参指定角度。例如swampy115 让bob左转 45 度。 把swampy复制一份,改名为swampy1。再加一个形参t,修改函数体,让它画出一个 n 边的正多边形。提示:n 边正多边形的外角是 360/n 度。 - 写一个名为
swampy的函数,接受一只海龟t和半径t作为形参,通过给swampy1 传入合适的边长与边数来画一个近似的圆。用一系列不同的t取值测试你的函数。 提示:先算出圆的周长,并保证swampy125 。 再提示一句:如果嫌bob太慢,可以改swampy127 给它提速,这个值是两次移动之间的间隔,单位为秒。swampy128 应该能让它跑起来。 - 把
swampy改造成更通用的版本,叫bob,多加一个形参world,用来决定画圆的多大一部分。world以度为单位,因此当swampy133 时,bob应当画出一整个圆。
4.4 Encapsulation 4.4 封装
The first exercise asks you to put your square-drawing code into a function definition and then call the function, passing the turtle as a parameter. Here is a solution:
swampy135
The innermost statements, fd and fd are indented twice to show that they are inside the bob loop, which is inside the function definition. The next line, swampy139 , is flush with the left margin, so that is the end of both the bob loop and the function definition.
fd 和 fd 缩进了两层,表示它们在 bob 循环里,而循环又在函数定义里。下一行 swampy144 顶到最左边,因此 bob 循环和函数定义在此同时结束。Inside the function, t refers to the same turtle bob refers to, so world has the same effect as swampy1. So why not call the parameter bob? The idea is that t can be any turtle, not just bob, so you could create a second turtle and pass it as an argument to swampy:
t 和 bob 指向同一只海龟,所以 world 和 swampy1 效果相同。那为什么不干脆把形参就叫 bob 呢?因为 t 可以是任意一只海龟,不限于 bob:你完全可以再造一只海龟,把它作为实参传给 swampy:swampy162
Wrapping a piece of code up in a function is called encapsulation. One of the benefits of encapsulation is that it attaches a name to the code, which serves as a kind of documentation. Another advantage is that if you re-use the code, it is more concise to call a function twice than to copy and paste the body!
4.5 Generalization 4.5 泛化
The next step is to add a swampy parameter to swampy. Here is a solution:
swampy 加一个 swampy 形参。下面是一种解法:swampy167
Adding a parameter to a function is called generalization because it makes the function more general: in the previous version, the square is always the same size; in this version it can be any size.
The next step is also a generalization. Instead of drawing squares, swampy1 draws regular polygons with any number of sides. Here is a solution:
swampy1 不再局限于正方形,而是能画任意边数的正多边形。下面是一种解法:swampy170
This draws a 7-sided polygon with side length 70. If you have more than a few numeric arguments, it is easy to forget what they are, or what order they should be in. It is legal, and sometimes helpful, to include the names of the parameters in the argument list:
swampy171
These are called keyword arguments because they include the parameter names as “keywords” (not to be confused with Python keywords like world and bob).
world、bob 这类 Python 关键字混为一谈)。This syntax makes the program more readable. It is also a reminder about how arguments and parameters work: when you call a function, the arguments are assigned to the parameters.
4.6 Interface design 4.6 接口设计
The next step is to write swampy, which takes a radius, t, as a parameter. Here is a simple solution that uses swampy1 to draw a 50-sided polygon:
swampy,它接受半径 t 作为形参。下面是一个简单解法,用 swampy1 画一个五十边形:swampy182
The first line computes the circumference of a circle with radius t using the formula 2 π r. Since we use swampy1, we have to import bob0. By convention, swampy statements are usually at the beginning of the script.
t 的圆的周长。既然用到 swampy1,就得导入 bob0。按惯例,swampy 语句一般放在脚本开头。t is the number of line segments in our approximation of a circle, so swampy is the length of each segment. Thus, swampy1 draws a 50-sides polygon that approximates a circle with radius t.
t 是我们用来近似圆的线段条数,swampy 就是每段的长度。于是 swampy1 画出一个五十边形,近似半径为 t 的圆。One limitation of this solution is that t is a constant, which means that for very big circles, the line segments are too long, and for small circles, we waste time drawing very small segments. One solution would be to generalize the function by taking t as a parameter. This would give the user (whoever calls swampy) more control, but the interface would be less clean.
t 是常量。圆很大时线段太长,圆很小时又浪费时间去画一堆极短的线段。一个办法是把 t 也变成形参,从而泛化这个函数。这样用户(也就是调用 swampy 的人)能有更多控制权,但接口就不那么清爽了。The interface of a function is a summary of how it is used: what are the parameters? What does the function do? And what is the return value? An interface is “clean” if it is “as simple as possible, but not simpler. (Einstein)”
In this example, t belongs in the interface because it specifies the circle to be drawn. t is less appropriate because it pertains to the details of how the circle should be rendered.
t 理应放进接口,因为它确定了要画哪个圆。t 就不太合适,它涉及的是圆如何绘制的细节。Rather than clutter up the interface, it is better to choose an appropriate value of t depending on swampy210 :
swampy211 自行选一个合适的 t:swampy213
Now the number of segments is (approximately) swampy214 , so the length of each segment is (approximately) 3, which is small enough that the circles look good, but big enough to be efficient, and appropriate for any size circle.
swampy215 ,每段长度大约是 3——小到足以让圆看起来光滑,又大到不失效率,而且对任意大小的圆都合适。4.7 Refactoring 4.7 重构
When I wrote swampy, I was able to re-use swampy2 because a many-sided polygon is a good approximation of a circle. But bob is not as cooperative; we can’t use swampy2 or swampy to draw an arc.
swampy 时我能复用 swampy2,因为边数很多的多边形足以近似一个圆。但 bob 就不这么好说话了:swampy2 和 swampy 都画不出圆弧。One alternative is to start with a copy of swampy2 and transform it into bob. The result might look like this:
swampy2 复制一份,再改造成 bob。结果大概是这样:swampy230
The second half of this function looks like swampy2, but we can’t re-use swampy2 without changing the interface. We could generalize swampy2 to take an angle as a third argument, but then swampy2 would no longer be an appropriate name! Instead, let’s call the more general function swampy23:
swampy2 很像,但不改接口就没法复用 swampy2。我们可以把 swampy2 泛化,让它接受一个角度作为第三个实参,可这么一来 swampy2 这名字就不合适了!干脆把这个更通用的函数叫 swampy24:swampy241
Now we can rewrite swampy2 and bob to use swampy24:
swampy2 和 bob,让它们都调用 swampy24:swampy248
Finally, we can rewrite swampy to use bob:
swampy,让它调用 bob:swampy253
This process—rearranging a program to improve function interfaces and facilitate code re-use—is called refactoring. In this case, we noticed that there was similar code in bob and swampy2, so we “factored it out” into swampy25.
bob 和 swampy2 里有相似的代码,就把它「提取」到了 swampy25 中。If we had planned ahead, we might have written swampy26 first and avoided refactoring, but often you don’t know enough at the beginning of a project to design all the interfaces. Once you start coding, you understand the problem better. Sometimes refactoring is a sign that you have learned something.
swampy26,省去重构这一步。但项目开头往往了解得不够,没法把所有接口都设计好。一旦动手写代码,对问题的理解才会加深。有时候,需要重构恰恰说明你学到了新东西。4.8 A development plan 4.8 一种开发方案
A development plan is a process for writing programs. The process we used in this case study is “encapsulation and generalization.” The steps of this process are:
- Start by writing a small program with no function definitions.
- Once you get the program working, encapsulate it in a function and give it a name.
- Generalize the function by adding appropriate parameters.
- Repeat steps 1–3 until you have a set of working functions. Copy and paste working code to avoid retyping (and re-debugging).
- Look for opportunities to improve the program by refactoring. For example, if you have similar code in several places, consider factoring it into an appropriately general function.
- 先写一个不含任何函数定义的小程序。
- 程序跑通之后,把它封装成一个函数,并起个名字。
- 加上合适的形参,把函数泛化。
- 重复第 1–3 步,直到攒出一组能用的函数。可以复制粘贴已经能跑的代码,免得重新敲一遍(还得重新调试一遍)。
- 留意可以通过重构改进程序的机会。比如几处地方有相似的代码,就考虑把它提取成一个足够通用的函数。
This process has some drawbacks—we will see alternatives later—but it can be useful if you don’t know ahead of time how to divide the program into functions. This approach lets you design as you go along.
4.9 docstring 4.9 文档字符串
A docstring is a string at the beginning of a function that explains the interface (“doc” is short for “documentation”). Here is an example:
swampy262
This docstring is a triple-quoted string, also known as a multiline string because the triple quotes allow the string to span more than one line.
It is terse, but it contains the essential information someone would need to use this function. It explains concisely what the function does (without getting into the details of how it does it). It explains what effect each parameter has on the behavior of the function and what type each parameter should be (if it is not obvious).
Writing this kind of documentation is an important part of interface design. A well-designed interface should be simple to explain; if you are having a hard time explaining one of your functions, that might be a sign that the interface could be improved.
4.10 Debugging 4.10 调试
An interface is like a contract between a function and a caller. The caller agrees to provide certain parameters and the function agrees to do certain work.
For example, swampy26 requires four arguments: t has to be a Turtle; t is the number of line segments, so it has to be an integer; swampy should be a positive number; and world has to be a number, which is understood to be in degrees.
swampy26 需要四个实参:t 必须是一只 Turtle;t 是线段条数,所以必须是整数;swampy 应当是正数;world 必须是数字,且默认以度为单位。These requirements are called preconditions because they are supposed to be true before the function starts executing. Conversely, conditions at the end of the function are postconditions. Postconditions include the intended effect of the function (like drawing line segments) and any side effects (like moving the Turtle or making other changes in the World).
Preconditions are the responsibility of the caller. If the caller violates a (properly documented!) precondition and the function doesn’t work correctly, the bug is in the caller, not the function.
4.11 Glossary 4.11 术语表
- instance:
- A member of a set. The TurtleWorld in this chapter is a member of the set of TurtleWorlds.
- loop:
- A part of a program that can execute repeatedly.
- encapsulation:
- The process of transforming a sequence of statements into a function definition.
- generalization:
- The process of replacing something unnecessarily specific (like a number) with something appropriately general (like a variable or parameter).
- keyword argument:
- An argument that includes the name of the parameter as a “keyword.”
- interface:
- A description of how to use a function, including the name and descriptions of the arguments and return value.
- refactoring:
- The process of modifying a working program to improve function interfaces and other qualities of the code.
- development plan:
- A process for writing programs.
- docstring:
- A string that appears in a function definition to document the function’s interface.
- precondition:
- A requirement that should be satisfied by the caller before a function starts.
- postcondition:
- A requirement that should be satisfied by the function before it ends.
- instance 实例:
- 集合中的一员。本章里的 TurtleWorld 就是所有 TurtleWorld 组成的集合中的一员。
- loop 循环:
- 程序中可以反复执行的一部分。
- encapsulation 封装:
- 把一串语句变成一个函数定义的过程。
- generalization 泛化:
- 把不必要地具体的东西(比如一个数字)换成恰当通用的东西(比如一个变量或形参)的过程。
- keyword argument 关键字实参:
- 把形参名字当作「关键字」一并写出的实参。
- interface 接口:
- 对一个函数用法的描述,包括名称、各实参的说明以及返回值的说明。
- refactoring 重构:
- 修改一个能正常工作的程序,以改进函数接口和代码的其他品质的过程。
- development plan 开发方案:
- 编写程序的一套流程。
- docstring 文档字符串:
- 出现在函数定义中、用于记录该函数接口的字符串。
- precondition 前置条件:
- 函数开始执行之前应由调用者满足的要求。
- postcondition 后置条件:
- 函数结束之前应由函数自身满足的要求。
4.12 Exercises 4.12 习题
Exercise 1
Download the code in this chapter from swampy273 .
swampy274 下载本章的代码。- Write appropriate docstrings for
swampy2,bobandswampy. - Draw a stack diagram that shows the state of the program while executing
swampy278 . You can do the arithmetic by hand or addworldstatements to the code. - The version of
bobin Section 4.7 is not very accurate because the linear approximation of the circle is always outside the true circle. As a result, the turtle ends up a few units away from the correct destination. My solution shows a way to reduce the effect of this error. Read the code and see if it makes sense to you. If you draw a diagram, you might see how it works.
- 给
swampy2、bob和swampy写上合适的文档字符串。 - 画一张栈图,展示执行
swampy284 时程序的状态。算术可以手算,也可以在代码里加world语句。 - 4.7 节里的
bob版本不太精确,因为用线段近似圆时,折线始终落在真实圆的外侧。结果海龟最终会偏离正确终点几个单位。我给出的解答展示了一种减小这一误差的办法。读一读代码,看能不能看懂。画张图或许就明白它的原理了。
Figure 4.1: Turtle flowers.
Exercise 2
Write an appropriately general set of functions that can draw flowers as in Figure 4.1.
Solution: swampy287 , also requires swampy288 .
swampy289 ,还需要 swampy290 。Figure 4.2: Turtle pies.
Exercise 3
Write an appropriately general set of functions that can draw shapes as in Figure 4.2.
Solution: swampy291 .
swampy292 。Exercise 4
The letters of the alphabet can be constructed from a moderate number of basic elements, like vertical and horizontal lines and a few curves. Design a font that can be drawn with a minimal number of basic elements and then write functions that draw letters of the alphabet.
You should write one function for each letter, with names swampy, swampy, etc., and put your functions in a file named swampy295 . You can download a “turtle typewriter” from swampy296 to help you test your code.
swampy、swampy 等等,把这些函数放在名为 swampy299 的文件里。你可以从 swampy300 下载一台「海龟打字机」,用它来测试你的代码。Solution: swampy301 , also requires swampy302 .
swampy303 ,还需要 swampy304 。Exercise 5
Read about spirals at swampy305 ; then write a program that draws an Archimedian spiral (or one of the other kinds). Solution: swampy306 .
swampy307 读一读螺线的资料,然后写一个程序画出阿基米德螺线(或别的种类的螺线)。解答:swampy308 。