[自述: 感觉从这一章开始算是有点 "干货" 了, 很容易激起初学者的兴趣, 想当年上学的时候就是老师随便写的一个循环语句让两个大于号 ">>" 沿着屏幕绕圈就像贪吃蛇一样, 吸引了我并且让我入了写程序这个 "坑"]
第四章
学习案例: 接口设计
本章示例代码可以从这里下载:
- http://thinkpython.com/code/polygon.py
- 4.1 TurtleWorld
为了配合本书, 我写了一个 Swampy 包, 你可以从这里下载并按照上面的说明安装到你的系统: http://thinkpython.com/swampy
包是一个模块的集合, TurtleWorld 就是 Swampy 里面的一个模块, 它提供了一些引导小海龟在屏幕上画线的函数集. 你只要在系统中安装了 Swampy 包就可以导入 TurtleWorld 模块:
from swampy.TurtleWorld import *
如果你下载了 Swampy 包, 但是没有安装, 你可以在 Swampy 的根目录调试程序, 或者把该目录添加到 Python 可以搜索的路径中去, 然后再这样导入 TurtleWorldlike :
from TurtleWorld import *
安装过程和 Python 搜索路径的设置, 取决于你的系统, 本书就不作具体描述, 如有疑问请参考此链接: http://thinkpython.com/swampy
建立一个文件 mypolygon.py, 输入下面的代码:
- from swampy.TurtleWorld import *
- world = TurtleWorld()
- bob = Turtle()
- print bob
- wait_for_user()
第一行代码表示从 swampy 包导入 TurtleWorld 模块. 接下来一行建立一个 TurtleWorld 对象和 Turtle 对象分别赋值给变量 world 和 bob. 打印 bob 变量你会看到类似这样的结果:
<TurtleWorld.Turtle instance at 0xb7bfbf4c>
这表示 bob 变量引用了 TurtleWorld 模块的一个实例 Turtle. 从上下文中可以看出,"实例" 表示集合的一个成员, 这里的 Turtle 实例可以是一组或者集合中的一个.
这里的 wait_for_user 告诉 TurtleWorld 等待用户下一步操作, 尽管在这个案例中除了等用户关闭窗口之外并无其它.
TurtleWorld 提供了一些 turtle 转向的函数: fd 和 bk 用于前进和后退, lt 和 rt 用于左右转弯. 并且每一个 Turtle 对象都有一支 "笔", 可以落下或提起, 如果笔落下, 当 Turtle 对象移动的时候就会留下痕迹. 函数 pu 和 pd 分别表示 "提笔" 和 "落笔".
下面的代码可以画一个直角(代码放在建立的 bob 对象和 wait_for_user 函数之间):
- fd(bob, 100)
- lt(bob)
- fd(bob, 100)
第一行代码让 bob 向前移动 100, 第二行让它向左转弯. 当你运行程序的时候, 你就可以看到 bob 向东向北移动并留下了运行轨迹.
请修改代码画一个正方形. 在实现此功能之前请不要继续本章内容!
4.2 简单循环
你可能写了这样的代码(此处省略了建立 TurtleWorld 对象和调用 wait_for_user 函数)
- fd(bob, 100)
- lt(bob)
- fd(bob, 100)
- lt(bob)
- fd(bob, 100)
- lt(bob)
- fd(bob, 100)
我们还可以用 for 循环语句来实现重复的功能. 请添加以下代码到 mypolygon.py 脚本文件并运行:
- for i in range(4):
- print 'Hello!'
你应该可以看到下面的结果:
- Hello!
- Hello!
- Hello!
- Hello!
这个例子里面用到了 for 语句, 后面我们还会看到更多. 但是这足以让你重新编写画正方形的程序, 下面就是 for 语句实现的代码:
- for i in range(4):
- fd(bob, 100)
- lt(bob)
for 语句的语法类似函数定义, 它有一个以冒号结尾的头和缩进的正文, 正文内可以包含任意数量的语句.
for 语句有时被称为循环, 因为执行流程经过正文处理之后又回到循环的顶部. 在这个案例中, 正文内容被执行了 4 次.
这里的代码跟之前画正方形的代码有些不同, 因为在画出了正方形之后又进行了一次转弯, 这样会花费额外的处理时间, 但是如果是重复的动作, 这样会简化代码, 而且 for 循环的这个版本让 Turtle 回到原点之后也恢复了初始的方向.
4.3 练习
下面是 TurtleWorld 系列的一些练习, 这些本来是为了好玩, 但是其中也不乏一些编程思想. 当你在练习的时候请思考一下重点是什么.
本书提供了下面练习的解决方案, 你可以先尝试一下, 不要直接抄答案.
1. 编写一个名为 square 的函数, 它传递一个名为 t 的 turtle 对象参数, 实现用 turtle 对象画一个正方形. 编写一个函数调用, 将 bob 作为参数传递给 square, 然后再次运行程序.
2. 在 square 函数添加另一个名为 length 的参数. 修改函数内容, 实现所画正方形的边长度为 length, 然后修改函数调用, 加入第二个参数, 再次运行程序. 使用一定长度范围的值来测试程序.
3. 默认情况下, lt 和 rt 函数进行 90 度旋转, 但您可以提供第二个参数, 指定角度的数量. 例如, lt(bob, 45) 可以让 bob 向左旋转 45 度. 复制一个 square 函数, 把它的名字改成 polygon. 再添加另一个名为 n 的参数并修改 polygon 函数主体, 使其绘制一个 n 边正多边形. 提示: n 边正多边形的外角是 360/n 度.
4. 编写一个名为 circle 的函数, 该函数以 turtle 对象 t 和半径 r 为参数, 通过调用具有适当长度和边数的多边形来绘制一个近似圆. 用一定范围的 r 来测试函数.
提示: 求出圆的周长, 确保 length * n = circumference.
另一个提示: 如果你觉得 bob 速度太慢, 你可以通过改变 bob.delay(移动间隔时间)来加快速度, 以秒为单位, 例如 bob.delay = 0.01.
5. 制作一个更通用的 circle 函数, 添加一个额外的参数 angle, 用来决定画一个圆弧的哪个部分. 以 angle 为单位, 当 angle=360 时, circle 函数就会画一个完整的圆.
4.4 封装
上文中的第一个练习要求将画正方形图形的代码放入函数定义中, 然后调用函数, 并将 turtle 作为参数传递. 解决方案如下:
- def square(t):
- for i in range(4):
- fd(t, 100)
- lt(t)
- square(bob)
在最内层的语句, fd 和 lt 缩进两次以表名它们位于 for 循环中, 而 for 循环位于函数定义中. 函数调用行 square(bob)与左侧空白齐平, 因此这是 for 循环和函数定义的结尾.
在函数内部, t 表示 Turtle 对象 bob, 因此 lt(t) 与 lt(bob) 具有相同的效果. 那这里为什么不直接调用参数 bob 呢? 这是因为这里的 t 可以是任何 Turtle 对象, 而不仅仅是 bob, 因为你可以创建另一个 Turtle 对象并将它作为参数传递给 square 函数:
- ray = Turtle()
- square(ray)
在函数中包装一段代码称为封装. 封装的好处之一是它可以将一个名称附加到代码上, 作为一种文档. 另一个优点是, 如果重用代码, 调用函数两次比复制和粘贴主体更简单方便!
4.5 泛化
接下来是给 square 函数添加一个参数 length. 实现代码如下:
- def square(t, length):
- for i in range(4):
- fd(t, length)
- lt(t)
- square(bob, 100)
向函数添加参数称为泛化, 因为它使函数更通用: 在以前的版本中, 正方形的大小是相同的; 在这个版本中, 它可以是可变的.
下一步也是泛化. polygon 函数是画出任意数量的正多边形, 而不是正方形. 实现代码如下:
- def polygon(t, n, length):
- angle = 360.0 / n
- for i in range(n):
- fd(t, length)
- lt(t, angle)
- polygon(bob, 7, 70)
这段代码将绘制一个边长度为 70 的 7 边形. 如果函数有多个数值参数, 那将会很容易忘记它们是什么, 或者它们应该处于什么顺序. 在参数列表中包含参数的名称是合法的, 有时是很有帮助的:
polygon(bob, n=7, length=70)
这些被称为关键字参数, 因为它们包含参数名作为 "关键字"(不要与 Python 关键字, 如 while 和 def 等混淆).
这种语法使程序更具可读性, 还提醒了参数和参数是如何工作的: 当您调用一个函数时, 实参数被分配给形参.
4.6 接口设计
下一步就是写以半径 r 为参数的 circle 函数. 这里有一个简单的解决方案, 就是用 polygon 函数画一个 50 面的多边形.
- def circle(t, r):
- circumference = 2 * math.pi * r
- n = 50
- length = circumference / n
- polygon(t, n, length)
第一行计算圆的周长, 公式为 2π*r. 因为我们使用到了 pi 值, 所以需要导入 math 模块. 按照惯例, 通常 import 语句都位于脚本的开头.
n 是画一个圆需要的近似的线的段数, 所以 length 是每个线段的长度. 因此, polygon 函数绘制了一个 50 边的多边形, 它近似于一个半径为 r 的圆.
这个解决方案有一个限制就是 n 是常数, 这意味着对于很大的圆, 每一个线段太长, 对于小的圆来说又浪费时间画很小的线段. 因此, 一个解决方案是把 n 作为参数来泛化这个函数. 这将给用户 (无论谁调用 circle 函数) 更多的控制, 但界面将会变得有些乱.
函数的接口是如何使用它: 参数是什么? 函数可以做什么? 返回值是多少? 如果接口 "尽可能简单, 但不简单", 那么它就是 "精炼" 的.(爱因斯坦)
在本例中, 变量 r 属于接口, 因为它指定了要绘制的圆. n 则不是, 因为它是函数内部如何渲染圆的局部变量.
因此, 与其让界面混乱, 还不如根据周长选择合适的 n 的值:
- def circle(t, r):
- circumference = 2 * math.pi * r
- n = int(circumference / 3) + 1
- length = circumference / n
- polygon(t, n, length)
现在画线的段数是(大约)circumference/3, 所以每个段的长度是(大约)3, 每一段线的长度足够小, 这样画出来的圆看起来才平滑, 只要线的段数大到足够有效, 就可以画出任何大小的圆.
4.7 重构
当我在写 circle 函数的时候我可以重用 polygon 函数, 应为一个许多边的多边形就是一个近似的圆. 但是圆弧却不能重用 circle 和 polygon 函数.
有一个可选的方法就是复制一份 polygon 函数, 再改成 arc 函数. 改完之后大致如下:
- def arc(t, r, angle):
- arc_length = 2 * math.pi * r * angle / 360
- n = int(arc_length / 3) + 1
- step_length = arc_length / n
- step_angle = float(angle) / n
- for i in range(n):
- fd(t, step_length)
- lt(t, step_angle)
这个函数的后半部分看起来像 polygon 函数, 但是我们不能在不改变接口的情况下重用 polygon 函数. 我们可以泛化 polygon 函数以一个角度作为第三个参数, 但 polygon 将不再是一个合适的函数名字! 我们可以用一个更通用的函数名称 polyline:
- def polyline(t, n, length, angle):
- for i in range(n):
- fd(t, length)
- lt(t, angle)
因此可以把 polyline 函数重新改写 polygon 函数和 arc 函数:
- def polygon(t, n, length):
- angle = 360.0 / n
- polyline(t, n, length, angle)
- def arc(t, r, angle):
- arc_length = 2 * math.pi * r * angle / 360
- n = int(arc_length / 3) + 1
- step_length = arc_length / n
- step_angle = float(angle) / n
- polyline(t, n, step_length, step_angle)
最后, 我们可以用 arc 函数重写 circle 函数:
- def circle(t, r):
- arc(t, r, 360)
这个过程 -- 重新安排程序以改进功能接口并促进代码重用可以称为 "重构". 在本例中, 我们注意到在 arc 和 polygon 函数中有类似的代码, 因此我们将其分解为 polyline 函数.
如果我们提前规划代码, 我们可能会首先编写 polyline 函数并避免重构, 但通常在项目开始的时候, 您对程序设计中所需要的接口还不够了解. 只有在开始编写代码之后, 您才会更好地理解问题. 某种程度上来说, 当你开始重构的函数的时候标志着你已经学会了一些东西了.
4.8 开发方案
开发计划是一个编写程序的过程. 我们在本案例研究中使用的过程是 "封装和泛化". 这项工作的步骤如下:
首先编写一个没有函数定义的小程序.
一旦程序可以正常运行, 再把它封装在一个函数中, 并且给函数起个名字.
通过添加适当的参数来拓展该函数.
重复步骤 1-3, 直到你有一个函数的集合. 复制并粘贴工作代码, 以避免重复输入(和重新调试).
通过重构寻找改进程序的机会. 例如, 如果您在几个地方有类似的代码, 考虑将其分解为适当的通用函数.
这个过程是有一些缺点的 -- 我们在本书的后面会有替代方案 -- 如果你不知道如何将程序划分为函数, 这也不影响你继续本书的学习.
4.9 文档字符串
docstring 是函数开头的一个字符串, 用于解释接口("doc" 是 "documentation" 的缩写). 这里有一个例子:
- def polyline(t, n, length, angle):
- """Draws n line segments with the given length and
- angle (in degrees) between them. t is a turtle.
- """
- for i in range(n):
- fd(t, length)
- lt(t, angle)
这个 docstring 是一个用三引号括起来的字符串, 也称为多行字符串, 因为三元引号允许字符串跨越多行.
它很简洁, 但是它包含了一些函数所需要的重要信息. 它简明地解释了函数的作用(没有详细介绍它是如何完成的). 它解释了每个参数对函数行为的影响, 以及每个参数应该是什么类型(如果不是很明显的话).
编写这种文档是接口设计的一个重要部分. 设计良好的接口应该很容易解释; 如果您在解释您的某个函数时遇到了困难, 这意味着这个接口还可以再改进.
4.10 调试
接口就像函数和调用者之间的契约. 调用方同意提供某些参数, 该函数同意执行某些工作.
例如, polyline 函数需要四个参数: t 必须是 Turtle 对象, n 是线段的数目, 所以 n 必须是一个整数; length 应该是一个正数; angle 必须是一个数字, 表示角度的意思.
这些需求被称为先决条件, 因为它们应该在函数开始执行之前准备好. 相反, 函数末尾的条件是后置条件. 后置条件包括函数的预期效果 (比如画线段) 和任何副加作用(如移动 Turtle 或在 TurtleWorld 中进行其他修改).
先决条件是调用方的责任. 如果调用方违反了一个 (适当的文档化的) 先决条件, 并且函数不能正常工作, 那问题就在函数调用的地方, 而不是函数里面.
4.11 术语表
实例:
一个集合中的成员. 本章中的 TurtleWorld 是 TurtleWorld 集合的成员.
循环:
程序中可以重复执行的部分.
封装:
将语句序列转换为函数定义的过程.
概括:
用适当的通用 (如变量或参数) 替换不必要的特定对象 (如数字) 的过程.
关键参数:
包含参数名称作为 "关键字" 的参数.
接口:
描述如何使用一个函数, 包括参数的名称和描述以及返回值.
重构:
修改工作程序的过程, 以改进函数接口和代码的其他质量.
开发计划:
编写程序的过程.
文档字符串:
在函数定义中显示的用于记录函数接口的字符串.
先决条件:
函数启动前调用方应该满足的需求.
后置条件:
函数结束前应该满足的需求.
4.12 练习
练习 1
从 http://thinkpython.com/code/polygon.py 下载本章下载本章中的代码.
为 polygon,arc 和 circle 函数编写适当的文档.
绘制一个堆栈图, 显示执行圆时程序的状态(bob,radius). 您可以手工进行算术或向代码中添加打印语句.
第 4.7 节中弧的版本不是很精确, 因为圆的线性近似总是在真圆之外. 因此导致 Turtle 离正确的目的地还差了几个单位. 我给出的解决方案减小此错误影响的方法. 请阅读代码, 看看它对您是否有意义. 如果你画一个图表, 你可能会看清除它是如何工作的.
图 4.1
练习 2
编写一组适当的通用函数, 可以绘制如图 4.1 所示的图案.
解决方案:
- http://thinkpython.com/code/flor.py
- http://thinkpython.com/code/polygon.py
图 4.2
练习 3
编写一组适当的通用函数, 可以绘制如图 4.2 所示的形状.
解决方案: http://thinkpython.com/code/pie.py
练习 4
字母表中的字母可以用一定数量的基本元素构成, 如垂直线和水平线以及一些曲线. 设计一种字体, 它可以用最少的基本元素绘制, 然后编写绘制字母的函数.
您需要为每个字母编写一个函数, 并命名为 draw_a, draw_b, ... 等, 并将您的函数放入名为 letters.py 的文件. 你可以从可以从 http://thinkpython.com/code/typewriter.py 下载一个下载一个 "Turtle 打字机" 来帮助你测试你的代码.
解决方案:
- http://thinkpython.com/code/letters.py
- http://thinkpython.com/code/polygon.py
练习 5
- http://thinkpython.com/code/spiral.py.
- # 英文版权 Allen B. Downey
- # 翻译中文版权 Simba Gu
- # 转载请注明出处
来源: https://www.cnblogs.com/simba/p/9964118.html