接口设计,主要是为了设计出相互配合的函数;
4.1 turtle模块
检查是否安装了turtle模块:
# turtle 模块(小写的t)提供了一个叫作 Turtle 的函数(大写的T),这个函数会创建一个 Turtle 对象
import turtle
bob = turtle.Turtle()
print(bob)
turtle.mainloop() # mainloop 告诉窗口等待用户操作
这个时候会出现下面的窗口:【只要可以import即是代表已经安装了turtle模块】
turtle模块的基本函数
fd
方法是前进,实参是像素距离;bk
方法是后退,实参是像素距离;lt
和rt
代表向左转和向右转,实参数角度;pu
和pd
分别代表抬笔和落笔,无参数;
下面画一个直角:
import turtle
bob = turtle.Turtle()
bob.pd()
bob.fd(100)
bob.lt(90)
bob.fd(100)
turtle.mainloop()
4.2 简单的重复【for循环(loop)】
示例:
# 绘制一个正方形
import turtle
bob = turtle.Turtle()
def bob_draw():
bob.pd()
bob.fd(100)
bob.lt(90)
bob.fd(100)
bob.lt(90)
for i in range(2):
bob_draw()
turtle.mainloop()
语法:
# num代表重复的次数
for i in range(num): # 冒号结尾的header
'代码块' # 缩进的语句体body
练习:
import turtle as T
import math
PI = math.pi
def square(t, length):
for i in range(4):
t.fd(length)
t.lt(90)
def polygon(t, length, n):
for i in range(n):
t.fd(length)
t.lt(360/n)
def polygon(t, length, n, angle):
for i in range(int(n * angle /360)):
t.fd(length)
t.lt(360/n)
def circle(t, r):
length = (2*PI*r) / 360
polygon(t, length, 360)
def arc(t, r, angle):
length = (2*PI*r)/360
polygon(t, length, 360, angle)
bob = T.Turtle()
arc(bob, 100, 90)
4.3 封装
- 将一部分代码放在函数中称之为
封装(encapsulation)
;- 封装的好处之一,是可以为这些代码赋予一个名字,可以相当于某种文档说明【更容易理解阅读】;
- 另一个好处,是减少冗余代码;
4.4 泛化
- 为函数增加一个形参称之为
泛化(generalization)
;因为这样使得函数更具有通用性;- 当函数有较多的形参的时候,这时候很难记住这些参数的具体顺序,这时候可以在调用函数的时候,加上 形参的名称:
polygon(bob, n=7, length=70)
这些称之为关键字实参
【区别于关键字】,因为这些加上了形参名作为“关键字”;【实际上也提高了可读性】
4.5 接口设计
- 下面是一个接受r作为形参的circle函数:
import math
def circle(t, r):
circumference = 2 * math.pi * r
n = 50
length = circumference / n
polygon(t, 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)
4.6 重构
- 重新整理一个程序以
改进接口
和促进代码复用
,被称作重构(refactoring)
- 在一个项目开始的时候,你常常并不知道那么多,不能设计好全部的接口。 一旦你开始编码后,你才能更好地理解问题。
4.7 开发方案(development plan)
开发方案是编写程序的一个过程,这里我们用到了
封装和泛化
;
- 这个过程的步骤:
- 从写一个没有函数定义的小程序开始;
- 一旦该程序开始运行,找出其中
强相关
的部分,将他们封装在一个函数中,并取一个名字; - 通过
增加适当的形参
,泛化这个函数; - 重复1-3步,直到有一些可以正常运行的函数。复制粘贴有用的代码,避免重复输入和重新调试;
- 寻找机会通过
重构
改进程序;
4.8 文档字符串(docstring)
docstring
是位于函数开始位置的一个字符串,它解释了函数的接口;
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),但是应该包含别人使用这个函数的时候需要了解的关键信息;
- 说明这个函数做什么;【不包含具体细节】
- 解释每个形参对函数的行为的影响;
- 每个形参的类型【如果不明显】
- 文档是接口设计中很重要的一部分,一个设计良好的接口应该很容易进行解释,如果一个函数很难进行解释,说明这个函数还有改进空间;
4.9 调试
- 接口像是函数和调用者之间的合同;调用者同意提供合适的参数,函数同意完成相应的工作;
先决条件(Pre-conditions):调用者需要提供的参数;
后置条件(Post-conditions):函数预期的结果以及其他附带效果;
- 先决条件由调用者负责满足。如果调用者违反一个(已经充分记录文档的!) 先决条件,导致函数没有正确工作,则故障(bug)出现在调用者一方,而不是函数;
- 如果满足了先决条件,没有满足后置条件,故障就在函数一方。如果你的先决条件和后置条件都很清楚,将有助于调试。