
1.6 源码剖析
1.6.1 Turtle模块
Turtle 模块是 Python 标准库自带的一个模块,用于绘制二维图形。在使用 Turtle模块之前,需要使用import turtle语句导入该模块。
Turtle 在英文中的含义是海龟,可以想象一下:在画布的中间,有一只海龟四处行走,走过的痕迹就被绘制成了图形。在画布中,海龟的位置是以二维笛卡儿坐标的方式来确定的,也就是xy轴坐标,如图1-7所示。

图1-7 xy轴坐标
海龟的初始位置在原点(0,0)处。海龟的状态除位置外,还有方向,方向以与水平的角度来确定,逆时针的角度为正数,顺时针的角度为负数。海龟的初始位置在原点上,初始方向为0,也就是在原点上,向右朝向x正轴。
所以,海龟的状态可以用一个三元组(x,y,d)来表示,x和y分别代表横坐标和纵坐标,d代表方向。第1.5节科赫曲线.py源码中主程序里的语句:

分别用三元组(x,y,d)来表示海龟在A点和B点的状态。在A点,海龟的横坐标为-450,纵坐标为0,方向为0,即水平向右。而在B点,海龟的横坐标为450,纵坐标为0,方向为0,即水平向右。海龟在AB之间的中间点C、D、E上的状态也同样是一个三元组。
下面列出了“科赫曲线.py”源码中所用到的Turtle模块的函数:

使用Turtle.setpos(x,y)改变海龟的位置,在默认情况下会在画布上留下痕迹,也就是一条直线。要想不留痕迹,就必须把海龟画笔抬起来,函数是Turtle.penup,到达指定位置后,把画笔放下来,函数是Turtle.pendown。


Pencolor和Bgcolor函数的参数color可以是字符串,如“red”“yellow”“white”等,也可以是一个RGB数字,如(255,255,0)。注意,在使用RGB数字前,必须调用Colormode函数指定颜色的模式:

可以使用Python自带的帮助文件来查看模块函数的具体说明。帮助文件打开的方式:单击“开始”按钮,选择【所有程序】→【Python 3.6】 → 【Python 3.6 Manuals (32-bit)】,显示窗口如图1-8所示。

图1-8 Python显示窗口
单击“索引”按钮,在输入框中输入“turtle”,在下拉列表中选择“turtle (module)”,如图1-9所示。

图1-9 在下拉列表中选择“turtle (module)”
窗口右边的文档中包含了Turtle模块所有函数的说明和示例。
1.6.2 函数
在计算机编程语言中,一个函数(Function)用来命名一个子程序(也就是一个语句序列集,这个语句序列集用于执行一些计算)。定义了一个函数,也就是指定了一个名称,以及与这个名称相关联的语句序列集。稍后,可以通过名称调用这个函数,即调用这个语句序列集,来执行相关的一些计算。除语句序列集外,函数还有一个入口和一个出口,可以在入口输入一些参数,就像将参数放进一个机器里,然后进行一系列处理,再从出口输出一个成品,这个成品叫作返回值(return value)。
比如,以下Turtle模块中的Left函数:
turtle.left(angle)
这个函数的名称为Left,括号里是它的输入参数,即角度angle。该函数接收参数angle,进行处理后返回的结果(即返回值)为海龟改变了方向后的状态,即一个三元组(x,y,d+angle)。调用这个函数后,海龟的方向向左(即逆时针)旋转angle度。
函数定义包括函数的名称及函数被调用时运行的语句序列集,定义方式如下:
def restore(p):
语句序列集
def 是一个关键字,表示这是一个函数定义,函数的名称为 Restore,接收的参数为p。函数的第一行叫作header(头),其余部分叫作body(体)。header以一个冒号(:)结束,body可以包含任意数量的语句。
把一个复杂的程序分割、打包成函数,主要有以下几个好处。
(1)通过自定义一个函数,可以命名一组语句,这样能使程序更易于阅读。
(2)重复的代码可以包装在一个函数中。一个函数只要编写一次,调试成功后,就可以重复使用在单个或多个程序中。如果稍后需要修改,也只需要在一个地方(函数定义的地方)修改。
(3)一个复杂的程序被分割成几个简短的函数后,就可以对每个函数单独进行调试,调试成功后,再组合成一个完整的程序。这样方便调试,也容易定位错误。
在“科赫曲线.py”源码中,自定义了三个函数,分别为Restore(p)、Get_point和Generator(A,B,L,n),具体介绍如下。
(1)函数Restore(p)可以将海龟的当前状态设置为参数所指定的状态。该函数只有一个输入参数p,为海龟的一个状态,即一个三元组(x,y,d),x和y分别代表横坐标和纵坐标,d代表方向。该函数没有返回值。
(2)函数Get_point用来获取海龟当前的状态。该函数没有输入参数,但是有返回值,即一个三元组(x,y,d)。
(3)函数Generator用来在主程序中生成科赫曲线。Generator(A,B,L,n)函数包含4个输入参数:A为起始点海龟的状态,也就是一个三元组(x,y,d);B为结束点海龟的状态,也是一个三元组;L为线段AB的长度;n为递归次数。该函数的返回值是True,这个返回值只代表绘图完成,并没有太大的意义。可以看到,在主程序中,A、B、L、n分别被赋值,并作为参数传递给Generator函数,Generator函数根据参数值采用递归的方式自动绘制科赫曲线。
1.6.3 递归算法
“科赫曲线.py”源码中的 Generator 函数使用递归算法生成科赫曲线。递归(Recursion)是计算机编程的一种基本算法,指的是在函数所包含的语句序列集中调用自身的一种方法。递归采用的策略:先由上往下,原问题层层分解成子问题;再由下往上,子问题层层解决,直至最终解决原问题。所以,只需要用少量的程序就可以描述复杂问题的解题过程,程序的代码量和复杂度都会大大地减少。
构成递归所需要具备的条件如下。
(1)子问题的定义应该和原问题的定义一样,也就是子问题是原问题的简化和缩小。
(2)递归必须有一个出口,不能无限循环,也就是必须有一个终点,在这个终点,运行完成后就会退出程序。
也就是说,递归是有去有回的。“有去”指的是递归问题可以层层分解成子问题,这些子问题的定义与原问题的定义相同,都可以采用相同的方法来解决。“有回”指的是问题的分解不会无休止,而是会到达一个终点,从这个终点开始,问题不再分解,而是逐步解决,下层的问题解决了,上一层的问题也能解决,如此顺着原路返回,直到返回原点,将原问题解决。
下面举一个耳熟能详的递归故事。
“从前有座山,山里有座庙,庙里有一个老和尚和一个小和尚,小和尚要老和尚讲故事,老和尚说从前有座山,山里有座庙,庙里有一个老和尚和一个小和尚,小和尚要老和尚讲故事,老和尚说从前有座山,山里有座庙………”
这个故事里显示了三层嵌套,每一层的“老和尚”和“小和尚”实际上是不同的,可以说,第二层的“老和尚”和“小和尚”是第一层故事里的人物,可以看成是第一层人物的缩小版,同样,第三层的“老和尚”和“小和尚”又是第二层故事里的人物。如果这个故事继续讲下去,就会无限循环,但是这并不是递归。递归不能无休止地调用自身,必须在某一个点“小和尚不要老和尚讲故事了”,事件终止退出这个子故事,这个子故事导致上一层的子故事终止退出,如此顺着原路返回,直到返回开始的故事,将开始的故事终止退出。
自相似性本身就是一种递归,所以可以采用递归算法来生成分形图形。我们来看一下源码中的Generator函数,在这个函数的定义中4次调用了自身,具体如下。


Generator 函数就是一个 if...else 语句, n 为迭代次数,当 n 不为1时,Generator(A,B,L,n)会获取线段AB之间的中间点C、D、E的海龟状态,按着绘图顺序在缩小了三分之一的线段AC、CD、DE、EB上调用Generator函数,迭代次数减1;接下来,如果n还是不为1,那么Generator(A,C,L/3,n-1)会获取线段AC之间的中间点C1、D1、E1的海龟状态,按着绘图顺序在缩小了九分之一的线段 AC1、C1D1、D1E1、E1C 上调用Generator函数,迭代次数减1;然后是Generator(C,D,L/3,n-1)、Generator(D,E,L/3,n-1)、Generator(E,B,L/3,n-1)。如此下去,直到迭代次数为1。而当n为1时就到达了这个递归的终点,只有在这个终点的时候,才会实际地绘制图形。
Turtle 模块封装了底层的数据处理逻辑,为了不在程序中包含太多的数学计算,使程序看起来更简单,在程序中采用了一种笨办法来获取中间点C、D、E的海龟状态,也就是用和背景色颜色相同的画笔在画布上画一遍生成元,在绘图过程中获取中间点的海龟状态并进行保存,最后将画笔颜色恢复原样。
下面,在纸上运行一下函数Generator(A,B,L,n)以帮助理解。
假设n为2,因为n不等于1,所以Generator(A,B,L,2)执行else子句:获取中间点C、D、E的位置和方向,不显示图形。然后4次调用自身:
调用函数Generator(A,C,L/3,n-1),n=2-1=1,执行if子句恢复海龟状态到A点,绘制 AC 段图形,Generator(A,C,L/3,n-1)终止,返回上层,继续执行函数 Generator (A,B,L,2)。
函数Generator(A,B,L,2)继续调用函数Generator(C,D,L/3,n-1),n等于1,执行if子句恢复海龟状态到C点,绘制CD段图形,Generator(C,D,L/3,n-1)终止,返回上层,继续执行函数Generator(A,B,L,2)。
函数Generator(A,B,L,2)继续调用函数Generator(D,E,L/3,1),n等于1,执行if子句恢复海龟状态到D点,绘制DE段图形,Generator(D,E,L/3,1)终止,返回上层,继续执行函数Generator(A,B,L,2)。
函数Generator(A,B,L,2)继续调用函数Generator(E,B,L/3,1),n等于1,执行if子句恢复海龟状态到E点,绘制EB段图形,Generator(E,B,L/3,1)终止,返回上层,继续执行函数Generator(A,B,L,2)。
函数Generator(A,B,L,2)没有执行语句了,结束。