学习参考书籍
编写比较通用的一个可以画出像图中那样花朵的函数集。
import math
import turtle
def polyline(t, n, length, angle):
"""Draws n line segments.
t: Turtle object
n: number of line segments
length: length of each segment
angle: degrees between segments
"""
for i in range(n):
t.fd(length)
t.lt(angle)
def arc(t, r, angle):
"""Draws an arc with the given radius and angle.
t: Turtle
r: radius
angle: angle subtended by the arc, in degrees
"""
arc_length = 2 * math.pi * r * abs(angle) / 360
n = int(arc_length / 4) + 3
step_length = arc_length / n
step_angle = float(angle) / n
# making a slight left turn before starting reduces
# the error caused by the linear approximation of the arc
t.lt(step_angle/2)
polyline(t, n, step_length, step_angle)
t.rt(step_angle/2)
def petal(t, r, angle):
"""Draws a petal using two arcs.
t: Turtle
r: radius of the arcs
angle: angle (degrees) that subtends the arcs
"""
for i in range(2):
arc(t, r, angle)
t.lt(180-angle)
def flower(t, n, r, angle):
"""Draws a flower with n petals.
t: Turtle
n: number of petals
r: radius of the arcs
angle: angle (degrees) that subtends the arcs
"""
for i in range(n):
petal(t, r, angle)
t.lt(360.0/n)
def move(t, length):
"""Move Turtle (t) forward (length) units without leaving a trail.
Leaves the pen down.
"""
t.pu()
t.fd(length)
t.pd()
bob = turtle.Turtle()
# draw a sequence of three flowers, as shown in the book.
move(bob, -100)
flower(bob, 7, 60.0, 60.0)
move(bob, 100)
flower(bob, 10, 40.0, 80.0)
move(bob, 100)
flower(bob, 20, 140.0, 20.0)
bob.hideturtle()
turtle.mainloop()
运行结果:
编写比较通用的一个可以画出图中那样图形的函数集。
def isosceles(t,r,angle):
'''
画一个倒三角形,并且箭头起始和结束都指向右
'''
y=r*math.sin(angle*math.pi/180)
t.rt(angle)
t.fd(r)
t.lt(90+angle)
t.fd(2*y)
t.lt(90+angle)
t.fd(r)
t.lt(180-angle)
def polypie(t,n,r):
'''
画出饼状图函数
'''
angle = 360.0 / n
for i in range(n):
isosceles(t, r, angle/2)
t.lt(angle)
def draw_pie(t,n,r):
'''
画完后将光标右移
'''
polypie(t, n, r)
t.pu()
t.fd(r*2 + 10)
t.pd()
bob = turtle.Turtle()
bob.pu()
bob.bk(130)
bob.pd()
# draw polypies with various number of sides
size = 40
draw_pie(bob, 5, size)
draw_pie(bob, 6, size)
draw_pie(bob, 7, size)
draw_pie(bob, 8, size)
bob.hideturtle()
turtle.mainloop()
运行结果:
一、封装
将一部分代码包装在函数里被称作 encapsulation(封装)。 封装的好处之一,为这些代码赋予一个名字, 这充当了某种文档说明。另一个好处是,如果你重复使用这些代码, 调用函数两次比拷贝粘贴函数体要更加简洁!
函数功能:画一个正方形
def square(t):
for i in range(4):
t.fd(100)
t.lt(90)
square(bob)
二、泛化
为函数增加一个形参被称作泛化(generalization), 这使得函数更通用。
函数功能:在前面的版本中, 正方形的边长总是一样的;此版本中,它可以是任意大小。
def square(t, length):
for i in range(4):
t.fd(length)
t.lt(90)
square(bob, 100)
函数功能:可以画任意的正多边形
def polygon(t, n, length):
angle = 360 / n
for i in range(n):
t.fd(length)
t.lt(angle)
polygon(bob, 7, 70)
三、接口设计
函数功能:下一个练习是编写接受半径r作为形参的 circle 函数。 下面是一个使用 polygon 画一个50边形的简单解法。
import math
def circle(t, r):
circumference = 2 * math.pi * r
n = 50
length = circumference / n
polygon(t, n, length)
函数的第一行通过半径r计算圆的周长,公式是2πr。 由于用了 math.pi ,我们需要导入 math 模块。 按照惯例,import 语句通常位于脚本的开始位置。n是我们的近似圆中线段的条数, length 是每一条线段的长度。 这样 polygon 画出的就是一个50边形,近似一个半径为r的圆。这种解法的一个局限在于,n是一个常量,意味着对于非常大的圆, 线段会非常长,而对于小圆,我们会浪费时间画非常小的线段。 一个解决方案是将n作为形参,泛化函数。 这将给用户(调用 circle 的人)更多的掌控力, 但是接口就不那么干净了。
函数的接口(interface)是一份关于如何使用该函数的总结: 形参是什么?函数做什么?返回值是什么? 如果接口让调用者避免处理不必要的细节,直接做自己想做的事,那么这个接口就是“干净的”。
在这个例子中,r 属于接口的一部分,因为它指定了要画多大的圆。 n就不太合适,因为它是关于 如何 画圆的细节。
与其把接口弄乱,不如根据周长(circumference)选择一个合适的n值:
def circle(t, r):
circumference = 2 * math.pi * r
n = int(circumference / 3) + 1
length = circumference / n
polygon(t, n, length)
现在线段的数量,是约为周长三分之一的整型数, 所以每条线段的长度(大概)是3,小到足以使圆看上去逼真, 又大到效率足够高,对任意大小的圆都能接受。
四、重构
当我写 circle 程序的时候,我能够复用 polygon , 因为一个多边形是与圆形非常近似。 但是 arc 就不那么容易实现了;我们不能使用 polygon 或者 circle 来画一个弧。
一种替代方案是从复制 polygon 开始, 然后将它转化为 arc 。最后的函数看上去可像这样:
函数功能:画圆弧
arc_length = 2 * math.pi * r * angle / 360
n = int(arc_length / 3) + 1
step_length = arc_length / n
step_angle = angle / n
for i in range(n):
t.fd(step_length)
t.lt(step_angle)
该函数的后半部分看上去很像 polygon , 但是在不改变接口的条件下,我们无法复用 polygon 。 我们可以泛化 polygon 来接受一个角度作为第三个实参, 但是这样 polygon 就不再是一个合适的名字了! 让我们称这个更通用的函数为 polyline :
def polyline(t, n, length, angle):
for i in range(n):
t.fd(length)
t.lt(angle)
现在,我们可以用 polyline 重写 polygon 和 arc :
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)
重新整理一个程序以改进函数接口和促进代码复用的这个过程, 被称作重构(refactoring)。 在此例中,我们注意到 arc 和 polygon 中有相似的代码, 因此,我们“将它分解出来”(factor it out),放入 polyline 函数。
五、开发方案
开发计划(development plan)是一种编写程序的过程。 此例中我们使用的过程是“封装和泛化”。 这个过程的具体步骤是:
从写一个没有函数定义的小程序开始。
一旦该程序运行正常,找出其中相关性强的部分,将它们封装进一个函数并给它一个名字。
通过增加适当的形参,泛化该函数。
重复1–3步,直到你有一些可正常运行的函数。 复制粘贴有用的代码,避免重复输入(和重新调试)。
寻找机会通过重构改进程序。 例如,如果在多个地方有相似的代码,考虑将它分解到一个合适的通用函数中。
这个过程也有一些缺点。后面我们将介绍其他替代方案, 但是如果你事先不知道如何将程序分解为函数,这是个很有用办法。 该方法可以让你一边编程,一边设计。
六、文档字符串
文档字符串(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):
t.fd(length)
t.lt(angle)
按照惯例,所有的文档字符串都是三重引号(triple-quoted)字符串,也被称为多行字符串, 因为三重引号允许字符串超过一行。
它很简要(terse),但是包括了他人使用此函数时需要了解的关键信息。 它扼要地说明该函数做什么(不介绍背后的具体细节)。 它解释了每个形参对函数的行为有什么影响,以及每个形参应有的类型 (如果它不明显的话)。
写这种文档是接口设计中很重要的一部分。 一个设计良好的接口应该很容易解释, 如果你很难解释你的某个函数,那么你的接口也许还有改进空间。
七、调试
接口就像是函数和调用者之间的合同。 调用者同意提供合适的参数,函数同意完成相应的工作。
例如,polyline 函数需要4个实参:t 必须是一个 Turtle ; n 必须是一个整型数; length 应该是一个正数; angle 必须是一个数,单位是度数。
这些要求被称作先决条件(preconditions), 因为它们应当在函数开始执行之前成立(true)。 相反,函数结束时的条件是后置条件(postconditions)。 后置条件包括函数预期的效果(如画线段)以及任何其他附带效果 (如移动 Turtle 或者做其它改变)。
先决条件由调用者负责满足。如果调用者违反一个(已经充分记录文档的!) 先决条件,导致函数没有正确工作,则故障(bug)出现在调用者一方,而不是函数。
如果满足了先决条件,没有满足后置条件,故障就在函数一方。如果你的先决条件和后置条件都很清楚,将有助于调试。