Python 教程 4 — 从0到1,学习Turtle并写一个优秀的应用

本章将通过一个案例研究,介绍如何设计出相互配合的函数。

本章会介绍 turtle 模块,它可以让你使用海龟图形(turtle graphics)绘制图像。大部分的Python安装环境下都包含了这个模块,但是如果你是在PythonAnywhere上运行Python的,你将无法运行本章中的代码示例(至少在我写这章时是做不到的)。

如果你已经在自己的电脑上安装了Python,那么不会有问题。如果没有,现在就是安装Python的好时机: 安装Python

文章结尾处有本章完整代码,请放心阅读。

1.turtle模块 #

打开Python解释器,输入以下代码,检查你是否安装了 turltle 模块:

>>> import turtle
>>> bob = turtle.Turtle()

上述代码运行后,应该会新建一个窗口,窗口中间有一个小箭头,代表的就是海龟。现在关闭窗口。

新建一个名叫 mypolygon.py 的文件,输入以下代码:

import turtle
bob = turtle.Turtle()
print(bob)
turtle.mainloop()

turtle 模块(小写的t)提供了一个叫作 Turtle 的函数(大写的T),这个函数会创建一个 Turtle 对象,我们将其赋值给名为 bob 的变量。打印 bob 的话,会输出下面这样的结果:

<turtle.Turtle object at 0xb7bfbf4c>

这意味着,bob 指向一个类型为Turtle的对象,这个类型是由 turtle 模块定义的。

mainloop 告诉窗口等待用户操作,尽管在这个例子中,用户除了关闭窗口之外,并没有其他可做的事情。

创建了一个 Turtle 对象之后,你可以调用 方法(method) 来在窗口中移动该对象。方法与函数类似,但是其语法略有不同。例如,要让海龟向前走:

bob.fd(100)

方法 fd 与我们称之为 bob 的对象是相关联的。调用方法就像提出一个请求:你在请求 bob 往前走。

fd 方法的实参是像素距离,所以实际前进的距离取决于你的屏幕。

Turtle 对象中你能调用的其他方法还包括:让它向后走的 bk ,向左转的 lt ,向右转的 rt 。 lt 和 rt 这两个方法接受的实参是角度。

另外,每个 Turtle 都握着一支笔,不是落笔就是抬笔;如果落笔了,Turtle 就会在移动时留下痕迹。pu 和 pd 这两个方法分别代表“抬笔(pen up)”和“落笔(pen down)”。

如果要画一个直角(right angle),请在程序中添加以下代码(放在创建 bob 之后,调用 mainloop 之前):

bob.fd(100)
bob.lt(90)
bob.fd(100)

当你运行此程序时,你应该会看到 bob 先朝东移动,然后向北移动,同时在身后留下两条线段(line segment)。

现在修改程序,画一个正方形。在没有成功之前,不要继续往下看。

2.简单的重复 #

很有可能你刚才写了像下面这样的一个程序:

bob.fd(100)
bob.lt(90)

bob.fd(100)
bob.lt(90)

bob.fd(100)
bob.lt(90)

bob.fd(100)

我们可以利用一个 for 语句,以更简洁的代码来做相同的事情。 将下面的示例代码加入 mypolygon.py ,并重新运行:

for i in range(4):
    print('Hello!')

你应该会看到如下输出:

Hello!
Hello!
Hello!
Hello!

这是 for 语句最简单的用法;后面我们会介绍更多的用法。 但是这对于让你重写画正方形的程序已经足够了。 如果没有完成,请不要往下看。

下面是一个画正方形的 for 语句:

for i in range(4):
    bob.fd(100)
    bob.lt(90)

for语句的语法和函数定义类似。 它有一个以冒号结尾的语句头(header)以及一个缩进的语句体(body)。 语句体可以包含任意条语句。

for 语句有时也被称为循环(loop),因为执行流程会贯穿整个语句体,然后再循环回顶部。 在此例中,它将运行语句体四次。

这个版本事实上和前面画正方形的代码有所不同,因为它在画完正方形的最后一条边后, 又多转了一下。这个额外的转动多花了些时间, 但是如果我们每次都通过循环来做这件事情,这样反而是简化了代码。 这个版本还让海龟回到了初始位置,朝向也与出发时一致。

3.练习 #

下面是一系列学习使用 Turtle 的练习。 这些练习虽说是为了好玩,但是也有自己的目的。 你在做这些练习的时候,想一想它们的目的是什么。译者注:原文中使用的还是 TurtleWorld ,应该是作者忘了修改。

后面几节中介绍了这些练习的答案,因此如果你还没完成(或者至少试过),请不要看答案。

  1. 写一个名为 square 的函数,接受一个名为 t 的形参,t 是一个海龟。 这个函数应用这只海龟画一个正方形。写一个函数调用,将 bob 作为实参传给 square ,然后再重新运行程序。
  2. 给 square 增加另一个名为 length 的形参。 修改函数体,使得正方形边的长度是 length ,然后修改函数调用,提供第二个实参。 重新运行程序。用一系列 length 值测试你的程序。
  3. 复制 square ,并将函数改名为 polygon 。 增加另外一个名为 n 的形参并修改函数体,让它画一个正n边形(n-sided regular polygon)。 提示:正n边形的外角是360/n360/n度。
  4. 编写一个名为 circle 的函数,它接受一个海龟t和半径r作为形参, 然后以合适的边长和边数调用 polygon ,画一个近似圆形。 用一系列r值测试你的函数。提示:算出圆的周长,并确保 length * n = circumference 。
  5. 完成一个更泛化(general)的 circle 函数,称其为 arc ,接受一个额外的参数 angle ,确定画多完整的圆。angle 的单位是度,因此当 angle=360 时, arc 应该画一个完整的圆。

4.封装 #

第一个练习要求你将画正方形的代码放到一个函数定义中,然后调用该函数, 将海龟作为形参传递给它。下面是一个解法:

def square(t):
    for i in range(4):
        t.fd(100)
        t.lt(90)

square(bob)

最内层的语句 fd 和 lt 被缩进两次,以显示它们处在 for 循环内, 而该循环又在函数定义内。下一行 square(bob) 和左边界(left margin)对齐, 表示 for 循环和函数定义结束。

在函数内部,t 指的是同一只海龟 bob , 所以 t.lt(90) 和 bob.lt(90) 的效果相同。 那么既然这样,为什么不将形参命名为 bob 呢? 因为 t 可以是任何海龟而不仅仅是 bob , 也就是说你可以创建第二只海龟,并且将它作为实参传递给 square :

alice = Turtle()
square(alice)

将一部分代码包装在函数里被称作 encapsulation(封装)。 封装的好处之一,为这些代码赋予一个名字, 这充当了某种文档说明。另一个好处是,如果你重复使用这些代码, 调用函数两次比拷贝粘贴函数体要更加简洁!

5.泛化 #

下一个练习是给 square 增加一个 length 形参。下面是一个解法:

def square(t, length):
    for i in range(4):
        t.fd(length)
        t.lt(90)

square(bob, 100)

为函数增加一个形参被称作泛化(generalization), 因为这使得函数更通用:在前面的版本中, 正方形的边长总是一样的;此版本中,它可以是任意大小。

下一个练习也是泛化。泛化之后不再是只能画一个正方形,polygon 可以画任意的正多边形。 下面是一个解法:

def polygon(t, n, length):
    angle = 360 / n
    for i in range(n):
        t.fd(length)
        t.lt(angle)

polygon(bob, 7, 70)

这个示例代码画了一个边长为70的七边形。

如果你在使用Python 2,angle 的值可能由于整型数除法(integer division)出现偏差。一个简单的解决办法是这样计算 angle :angle = 360.0 / n。因为分子(numerator)是一个浮点数,最终的结果也会是一个浮点数。

如果一个函数有几个数字实参,很容易忘记它们是什么或者它们的顺序。在这种情况下, 在实参列表中加入形参的名称是通常是一个很好的办法:

polygon(bob, n=7, length=70)

这些被称作关键字实参(keyword arguments), 因为它们加上了形参名作为“关键字”(不要和Python的关键字搞混了,如 while 和 def )。

这一语法使得程序的可读性更强。它也提醒了我们实参和形参的工作方式: 当你调用函数时,实参被赋给形参。

6.接口设计 #

下一个练习是编写接受半径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πr2π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,小到足以使圆看上去逼真, 又大到效率足够高,对任意大小的圆都能接受。

8.重构 #

当我写 circle 程序的时候,我能够复用 polygon , 因为一个多边形是与圆形非常近似。 但是 arc 就不那么容易实现了;我们不能使用 polygon 或者 circle 来画一个弧。

一种替代方案是从复制 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 = 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 :

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)

重新整理一个程序以改进函数接口和促进代码复用的这个过程, 被称作重构(refactoring)。 在此例中,我们注意到 arc 和 polygon 中有相似的代码, 因此,我们“将它分解出来”(factor it out),放入 polyline 函数。

如果我们提前已经计划好了,我们可能会首先写 polyline 函数,避免重构, 但是在一个项目开始的时候,你常常并不知道那么多,不能设计好全部的接口。 一旦你开始编码后,你才能更好地理解问题。 有时重构是一个说明你已经学到某些东西的预兆。

9.开发方案 #

开发计划(development plan)是一种编写程序的过程。 此例中我们使用的过程是“封装和泛化”。 这个过程的具体步骤是:

  1. 从写一个没有函数定义的小程序开始。
  2. 一旦该程序运行正常,找出其中相关性强的部分,将它们封装进一个函数并给它一个名字。
  3. 通过增加适当的形参,泛化该函数。
  4. 重复1–3步,直到你有一些可正常运行的函数。 复制粘贴有用的代码,避免重复输入(和重新调试)。
  5. 寻找机会通过重构改进程序。 例如,如果在多个地方有相似的代码,考虑将它分解到一个合适的通用函数中。

这个过程也有一些缺点。后面我们将介绍其他替代方案, 但是如果你事先不知道如何将程序分解为函数,这是个很有用办法。 该方法可以让你一边编程,一边设计。

10.文档字符串 #

文档字符串(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),但是包括了他人使用此函数时需要了解的关键信息。 它扼要地说明该函数做什么(不介绍背后的具体细节)。 它解释了每个形参对函数的行为有什么影响,以及每个形参应有的类型 (如果它不明显的话)。

写这种文档是接口设计中很重要的一部分。 一个设计良好的接口应该很容易解释, 如果你很难解释你的某个函数,那么你的接口也许还有改进空间。

11.调试 #

接口就像是函数和调用者之间的合同。 调用者同意提供合适的参数,函数同意完成相应的工作。

例如,polyline 函数需要4个实参:t 必须是一个 Turtle ; n 必须是一个整型数; length 应该是一个正数; angle 必须是一个数,单位是度数。

这些要求被称作先决条件(preconditions), 因为它们应当在函数开始执行之前成立(true)。 相反,函数结束时的条件是后置条件(postconditions)。 后置条件包括函数预期的效果(如画线段)以及任何其他附带效果 (如移动 Turtle 或者做其它改变)。

先决条件由调用者负责满足。如果调用者违反一个(已经充分记录文档的!) 先决条件,导致函数没有正确工作,则故障(bug)出现在调用者一方,而不是函数。

如果满足了先决条件,没有满足后置条件,故障就在函数一方。如果你的先决条件和后置条件都很清楚,将有助于调试。

12.术语表 #

方法(method):与对象相关联的函数,并使用点标记法(dot notation)调用。

循环(loop):程序中能够重复执行的那部分代码。

封装(encapsulation):将一个语句序列转换成函数定义的过程。

泛化(generalization):使用某种可以算是比较通用的东西(像变量和形参),替代某些没必要那么具体的东西(像一个数字)的过程。

关键字实参(keyword argument):包括了形参名称作为“关键字”的实参。

接口(interface):对如何使用一个函数的描述,包括函数名、参数说明和返回值。

重构(refactoring):修改一个正常运行的函数,改善函数接口及其他方面代码质量的过程。

开发计划(development plan):编写程序的一种过程。

文档字符串(docstring):出现在函数定义顶部的一个字符串,用于记录函数的接口。

先决条件(preconditions):在函数运行之前,调用者应该满足的要求。

ends.后置条件(postconditions):函数终止之前应该满足的条件。

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import math
import turtle


def square(t, length):
    """Draws a square with sides of the given length.
    Returns the Turtle to the starting position and location.
    """
    for i in range(4):
        t.fd(length)
        t.lt(90)


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 polygon(t, n, length):
    """Draws a polygon with n sides.
    t: Turtle
    n: number of sides
    length: length of each side.
    """
    angle = 360.0/n
    polyline(t, n, length, 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 circle(t, r):
    """Draws a circle with the given radius.
    t: Turtle
    r: radius
    """
    arc(t, r, 360)


# the following condition checks whether we are
# running as a script, in which case run the test code,
# or being imported, in which case don't.

if __name__ == '__main__':
    bob = turtle.Turtle()

    # draw a circle centered on the origin
    radius = 100
    bob.pu()
    bob.fd(radius)
    bob.lt(90)
    bob.pd()
    circle(bob, radius)

    # wait for the user to close the window
    turtle.mainloop()

练习题 #

习题 4-1 #

  1. 画一个执行 circle(bob, radius) 时的堆栈图(stack diagram),说明程序的各个状态。你可以手动进行计算,也可以在代码中加入打印语句。
  2. “重构”一节中给出的 arc 函数版本并不太精确,因为圆形的线性近似(linear approximation)永远处在真正的圆形之外。因此,Turtle 总是和正确的终点相差几个像素。我的答案中展示了降低这个错误影响的一种方法。阅读其中的代码,看看你是否能够理解。如果你画一个堆栈图的话,你可能会更容易明白背后的原理。

习题 4-2 #

使用Turtle绘制的花朵。

图4-1:使用Turtle绘制的花朵。

编写比较通用的一个可以画出像图4-1中那样花朵的函数集。

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import turtle

from polygon import arc


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()

答案中引入了polygon,是下面这份代码:

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import math
import turtle


def square(t, length):
    """Draws a square with sides of the given length.
    Returns the Turtle to the starting position and location.
    """
    for i in range(4):
        t.fd(length)
        t.lt(90)


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 polygon(t, n, length):
    """Draws a polygon with n sides.
    t: Turtle
    n: number of sides
    length: length of each side.
    """
    angle = 360.0/n
    polyline(t, n, length, 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 circle(t, r):
    """Draws a circle with the given radius.
    t: Turtle
    r: radius
    """
    arc(t, r, 360)


# the following condition checks whether we are
# running as a script, in which case run the test code,
# or being imported, in which case don't.

if __name__ == '__main__':
    bob = turtle.Turtle()

    # draw a circle centered on the origin
    radius = 100
    bob.pu()
    bob.fd(radius)
    bob.lt(90)
    bob.pd()
    circle(bob, radius)

    # wait for the user to close the window
    turtle.mainloop()

习题 4-3 #

图4-2:使用Turtle画的饼状图。

图4-2:使用Turtle画的饼状图。

编写比较通用的一个可以画出图4-2中那样图形的函数集。

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import math
import turtle


def draw_pie(t, n, r):
    """Draws a pie, then moves into position to the right.
    t: Turtle
    n: number of segments
    r: length of the radial spokes
    """
    polypie(t, n, r)
    t.pu()
    t.fd(r*2 + 10)
    t.pd()


def polypie(t, n, r):
    """Draws a pie divided into radial segments.
    t: Turtle
    n: number of segments
    r: length of the radial spokes
    """
    angle = 360.0 / n
    for i in range(n):
        isosceles(t, r, angle/2)
        t.lt(angle)


def isosceles(t, r, angle):
    """Draws an icosceles triangle.
    The turtle starts and ends at the peak, facing the middle of the base.
    t: Turtle
    r: length of the equal legs
    angle: half peak angle in degrees
    """
    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)


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()

习题 4-4 #

字母表中的字母可以由少量基本元素构成,例如竖线和横线,以及一些曲线。 设计一种可用由最少的基本元素绘制出的字母表,然后编写能画出各个字母的函数。

你应该为每个字母写一个函数,起名为draw_adraw_b等等, 然后将你的函数放在一个名为 letters.py 的文件里。 你可以从http://thinkpython2.com/code/typewriter.py 下载一个“海龟打字员”来帮你测试代码。

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import turtle

from polygon import circle, arc

# LEVEL 0 PRIMITIVES 
# fd, bk, lt, rt, pu, pd

def fd(t, length):
    t.fd(length)

def bk(t, length):
    t.bk(length)

def lt(t, angle=90):
    t.lt(angle)

def rt(t, angle=90):
    t.rt(angle)

def pd(t):
    t.pd()

def pu(t):
    t.pu()


# LEVEL 1 PRIMITIVES are simple combinations of Level 0 primitives.
# They have no pre- or post-conditions.

def fdlt(t, n, angle=90):
    """forward and left"""
    fd(t, n)
    lt(t, angle)

def fdbk(t, n):
    """forward and back, ending at the original position"""
    fd(t, n)
    bk(t, n)

def skip(t, n):
    """lift the pen and move"""
    pu(t)
    fd(t, n)
    pd(t)

def stump(t, n, angle=90):
    """Makes a vertical line and leave the turtle at the top, facing right"""
    lt(t)
    fd(t, n)
    rt(t, angle)

def hollow(t, n):
    """move the turtle vertically and leave it at the top, facing right"""
    lt(t)
    skip(t, n)
    rt(t)


# LEVEL 2 PRIMITIVES use primitives from Levels 0 and 1
# to draw posts (vertical elements) and beams (horizontal elements)
# Level 2 primitives ALWAYS return the turtle to the original
# location and direction.

def post(t, n):
    """Makes a vertical line and return to the original position"""
    lt(t)
    fdbk(t, n)
    rt(t)

def beam(t, n, height):
    """Makes a horizontal line at the given height and return."""
    hollow(t, n*height)
    fdbk(t, n)
    hollow(t, -n*height)

def hangman(t, n, height):
    """Makes a vertical line to the given height and a horizontal line
    at the given height and then return.
    This is efficient to implement, and turns out to be useful, but
    it's not so semantically clean."""
    stump(t, n * height)
    fdbk(t, n)
    lt(t)
    bk(t, n*height)
    rt(t)

def diagonal(t, x, y):
    """Makes a diagonal line to the given x, y offsets and return"""
    from math import atan2, sqrt, pi
    angle = atan2(y, x) * 180 / pi
    dist = sqrt(x**2 + y**2)
    lt(t, angle)
    fdbk(t, dist)
    rt(t, angle)

def vshape(t, n, height):
    diagonal(t, -n/2, height*n)
    diagonal(t, n/2, height*n)

def bump(t, n, height):
    """Makes a bump with radius n at height*n 
    """
    stump(t, n*height)
    arc(t, n/2.0, 180)
    lt(t)
    fdlt(t, n*height+n)


"""
The letter-drawing functions all have the precondition
that the turtle is in the lower-left corner of the letter,
and postcondition that the turtle is in the lower-right
corner, facing in the direction it started in.
They all take a turtle as the first argument and a size (n)
as the second.  Most letters are (n) units wide and (2n) units
high.
"""

def draw_a(t, n):
    diagonal(t, n/2, 2*n)
    beam(t, n, 1)
    skip(t, n)
    diagonal(t, -n/2, 2*n)

def draw_b(t, n):
    bump(t, n, 1)
    bump(t, n, 0)
    skip(t, n/2)

def draw_c(t, n):
    hangman(t, n, 2)
    fd(t, n)

def draw_d(t, n):
    bump(t, 2*n, 0)
    skip(t, n)

def draw_ef(t, n):
    hangman(t, n, 2)
    hangman(t, n, 1)

def draw_e(t, n):
    draw_ef(t, n)
    fd(t, n)

def draw_f(t, n):
    draw_ef(t, n)
    skip(t, n)

def draw_g(t, n):
    hangman(t, n, 2)
    fd(t, n/2)
    beam(t, n/2, 2)
    fd(t, n/2)
    post(t, n)

def draw_h(t, n):
    post(t, 2*n)
    hangman(t, n, 1)
    skip(t, n)
    post(t, 2*n)

def draw_i(t, n):
    beam(t, n, 2)
    fd(t, n/2)
    post(t, 2*n)
    fd(t, n/2)

def draw_j(t, n):
    beam(t, n, 2)
    arc(t, n/2, 90)
    fd(t, 3*n/2)
    skip(t, -2*n)
    rt(t)
    skip(t, n/2)

def draw_k(t, n):
    post(t, 2*n)
    stump(t, n, 180)
    vshape(t, 2*n, 0.5)
    fdlt(t, n)
    skip(t, n)

def draw_l(t, n):
    post(t, 2*n)
    fd(t, n)

def draw_n(t, n):
    post(t, 2*n)
    skip(t, n)
    diagonal(t, -n, 2*n)
    post(t, 2*n)

def draw_m(t, n):
    post(t, 2*n)
    draw_v(t, n)
    post(t, 2*n)

def draw_o(t, n):
    skip(t, n)
    circle(t, n)
    skip(t, n)

def draw_p(t, n):
    bump(t, n, 1)
    skip(t, n/2)

def draw_q(t, n):
    draw_o(t, n)
    diagonal(t, -n/2, n)

def draw_r(t, n):
    draw_p(t, n)
    diagonal(t, -n/2, n)

def draw_s(t, n):
    fd(t, n/2)
    arc(t, n/2, 180)
    arc(t, n/2, -180)
    fdlt(t, n/2, -90)
    skip(t, 2*n)
    lt(t)

def draw_t(t, n):
    beam(t, n, 2)
    skip(t, n/2)
    post(t, 2*n)
    skip(t, n/2)

def draw_u(t, n):
    post(t, 2*n)
    fd(t, n)
    post(t, 2*n)

def draw_v(t, n):
    skip(t, n/2)
    vshape(t, n, 2)
    skip(t, n/2)

def draw_w(t, n):
    draw_v(t, n)
    draw_v(t, n)

def draw_x(t, n):
    diagonal(t, n, 2*n)
    skip(t, n)
    diagonal(t, -n, 2*n)

def draw_v(t, n):
    skip(t, n/2)
    diagonal(t, -n/2, 2*n)
    diagonal(t, n/2, 2*n)
    skip(t, n/2)

def draw_y(t, n):
    skip(t, n/2)
    stump(t, n)
    vshape(t, n, 1)
    rt(t)
    fdlt(t, n)
    skip(t, n/2)

def draw_z(t, n):
    beam(t, n, 2)
    diagonal(t, n, 2*n)
    fd(t, n)

def draw_(t, n):
    # draw a space
    skip(t, n)

if __name__ == '__main__':

    # create and position the turtle
    size = 20
    bob = turtle.Turtle()

    for f in [draw_h, draw_e, draw_l, draw_l, draw_o]:
        f(bob, size)
        skip(bob, size)

    turtle.mainloop()

答案用到的另一个模块 polygon.py:

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import math
import turtle


def square(t, length):
    """Draws a square with sides of the given length.
    Returns the Turtle to the starting position and location.
    """
    for i in range(4):
        t.fd(length)
        t.lt(90)


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 polygon(t, n, length):
    """Draws a polygon with n sides.
    t: Turtle
    n: number of sides
    length: length of each side.
    """
    angle = 360.0/n
    polyline(t, n, length, 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 circle(t, r):
    """Draws a circle with the given radius.
    t: Turtle
    r: radius
    """
    arc(t, r, 360)


# the following condition checks whether we are
# running as a script, in which case run the test code,
# or being imported, in which case don't.

if __name__ == '__main__':
    bob = turtle.Turtle()

    # draw a circle centered on the origin
    radius = 100
    bob.pu()
    bob.fd(radius)
    bob.lt(90)
    bob.pd()
    circle(bob, radius)

    # wait for the user to close the window
    turtle.mainloop()

习题 4-5 #

阅读螺线(spiral)的相关知识:

螺线(Spiral),也称定倾曲线,是一类特殊曲线。它是切向量与一个固定的方向成定角的曲线。曲线为一般螺线的充分必要条件是它的挠率与曲率之比为常数,这类特殊曲线在力学工程技术中有着广泛的应用。螺线可分为螺旋线(非平面曲线)及平面螺线。
在空间,一个动点M沿直线L作匀速直线运动,同时又以等角速度绕同平面的轴线Oz旋转,M的轨迹是一条空间(非平面)曲线,称为螺旋线。它分为左旋与右旋两种。螺旋线是绕在圆柱面或圆锥面上的曲线,而它的切线与定直线(曲面的母线)的交角,是固定不变的。
对于平面螺线,是指在平面极坐标系中,如果极径ρ随极角θ的增加而成比例增加(或减少),这样的动点所形成的轨迹。典型的平面螺线有阿基米德螺线、对数螺线、双曲螺线等。

然后编写一个绘制阿基米德螺线(或者其他种类的螺线)的程序。

"""This module contains a code example related to
Think Python, 2nd Edition
by Allen Downey
http://thinkpython2.com
Copyright 2015 Allen Downey
License: http://creativecommons.org/licenses/by/4.0/
"""

from __future__ import print_function, division

import turtle


def draw_spiral(t, n, length=3, a=0.1, b=0.0002):
    """Draws an Archimedian spiral starting at the origin.
    Args:
      n: how many line segments to draw
      length: how long each segment is
      a: how loose the initial spiral starts out (larger is looser)
      b: how loosly coiled the spiral is (larger is looser)
    http://en.wikipedia.org/wiki/Spiral
    """
    theta = 0.0

    for i in range(n):
        t.fd(length)
        dtheta = 1 / (a + b * theta)

        t.lt(dtheta)
        theta += dtheta


# create the world and bob
bob = turtle.Turtle()
draw_spiral(bob, n=1000)

turtle.mainloop()

贡献者 #

  1. 翻译:@bingjin
  2. 校对:@bingjin
  3. 参考:@carfly

推荐阅读 #

推荐一个非常优惠的QMT开户渠道 #
很多喜欢玩量化的同学都想要找一个靠谱且低费率能做自动化的券商。 我之前也推荐过一个渠道,但是因为他们公司内部问题,之前的那个开户渠道也遗憾下线了,今天给大家找到了一个新的渠道,费率如下: 有需要的可以直接联系我的微信: 83493903 (

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。


​Python实用宝典 (pythondict.com)
关注公众号:Python实用宝典
更多精彩文章等你阅读

Powered by BetterDocs

评论(0)

提示:请文明发言

您的电子邮箱地址不会被公开。 必填项已用 * 标注