函数
在上一章中,我们使用了标准库提供的几个函数,如 int 和 float,以及 math 模块提供的几个函数,如 sqrt 和 pow。在本章中,你将学习如何创建自己的函数并运行它们。我们将看到一个函数如何调用另一个函数。我们还将介绍 for 循环,它用于重复执行计算。
定义新函数
函数定义指定了新函数的名称以及调用该函数时要执行的语句序列。下面是一个例子:
fn print_lyrics() {
print("I'm a lumberjack, and I'm okay.")
print("I sleep all night and I work all day.")
}fn 是一个关键字,表示这是一个函数定义。函数的名称是 print_lyrics。任何合法的变量名也是合法的函数名。
名称后面的空括号表示这个函数不接受任何参数。
函数定义的第一行称为函数头(header)——其余部分称为函数体(body)。函数头以冒号结束,函数体用大括号 {} 包围。这个函数的函数体包含两个 print 语句;一般来说,函数体可以包含任意数量的任意类型的语句。
定义函数会创建一个函数对象。既然我们已经定义了一个函数,就可以像调用内置函数一样调用它:
print_lyrics()当函数运行时,它执行函数体中的语句,显示"伐木工之歌"的前两行。
Python 程序员注意:
Auto 使用
fn代替 Python 的def,函数体用{}包围而不是缩进。a2p转译器会将fn转换为def并生成正确缩进的 Python 代码。
工作原理
我们使用 fn 关键字后跟函数名和括号来定义 print_lyrics。用 {} 包围的函数体包含两个 print 语句。在 main() 中,我们调用了两次 print_lyrics(),中间有一个空行。每次调用都会从上到下执行函数体。
参数
我们见过的一些函数需要参数;例如,调用 abs 时需要传递一个数字。有些函数接受多个参数;例如,math.pow 接受两个参数:底数和指数。
下面是一个接受参数的函数定义:
fn print_twice(string: str) {
print(string)
print(string)
}括号中的变量名是一个参数(parameter)。当函数被调用时,参数的值被赋给参数名。例如,我们可以这样调用 print_twice:
print_twice("Dennis Moore, ")你也可以使用变量作为参数:
let line = "Dennis Moore, "
print_twice(line)在这个例子中,line 的值被赋给了参数 string。
Python 程序员注意:
Auto 使用
string: str来声明带类型的参数。a2p转译器会去掉类型注解,在 Python 输出中生成string。如果你愿意,也可以在 Auto 中省略类型注解。
工作原理
print_twice 接受一个参数 string,并将其打印两次。repeat 接受两个参数 word 和 n,使用字符串的 * 运算符将单词重复 n 次后打印。
在 main() 中,我们分别用字面量字符串和变量调用 print_twice。我们还用一个字符串变量和一个整数调用 repeat。注意函数内部的参数名(string、word、n)与调用者的变量名(line、spam)是分开的。
调用函数
一旦定义了一个函数,就可以在另一个函数中使用它。为了演示,我们将编写打印"Spam Song"歌词的函数:
Spam, Spam, Spam, Spam, Spam, Spam, Spam, Spam, Spam, Spam, (Lovely Spam, Wonderful Spam!) Spam, Spam,
我们从上一节的 repeat 函数开始。然后可以定义基于它构建的更高层函数:
fn first_two_lines() {
repeat("Spam, ", 4)
repeat("Spam, ", 4)
}
fn last_three_lines() {
repeat("Spam, ", 2)
print("(Lovely Spam, Wonderful Spam!)")
repeat("Spam, ", 2)
}
fn print_verse() {
first_two_lines()
last_three_lines()
}当我们运行 print_verse 时,它调用 first_two_lines,后者又调用 repeat,后者又调用 print。这是很多层函数调用。当然,我们可以用更少的函数完成同样的功能,但这个例子的目的是展示函数如何协同工作。
重复
如果我们想显示多段歌词,可以使用 for 语句。下面是一个简单的例子:
for i in 0..2 {
print(i)
}for 语句的头部以关键字 for 开始,后跟一个新的变量名 i、关键字 in,以及一个范围表达式 0..2,它产生值 0 和 1。在 Auto 中,和许多编程语言一样,计数从 0 开始。
当 for 语句运行时,它将范围中的第一个值赋给 i,然后运行函数体,显示 0。当到达函数体末尾时,它循环回到头部——因此这个语句被称为循环(loop)。第二次循环时,它将下一个值赋给 i 并显示它。然后,因为那是范围中的最后一个值,循环结束。
下面是我们如何使用 for 循环打印歌曲的两段歌词:
for i in 0..2 {
print("Verse", i)
print_verse()
print()
}你可以将 for 循环放在函数内部。例如,print_n_verses 接受一个名为 n 的参数,并显示指定数量的歌词段落:
fn print_n_verses(n: int) {
for i in 0..n {
print_verse()
print()
}
}在这个例子中,我们没有在循环体中使用 i,但头部仍然需要一个变量名。
Python 程序员注意:
Auto 使用
0..n表示范围,而不是 Python 的range(n)。a2p转译器会自动将0..n转换为range(n)。Auto 没有while语句;请使用for配合范围或条件中断来代替。
工作原理
首先,一个简单的 for 循环使用 0..3 从 0 计数到 2。范围表达式 0..3 产生值 0、1 和 2——上界是排他的,就像 Python 的 range(3) 一样。
然后,第二个 for 循环使用 0..2 打印 Spam Song 的两段歌词。每次迭代调用 print_verse(),后者又调用 first_two_lines() 和 last_three_lines(),展示了函数如何组合起来,从更简单的部分构建更复杂的行为。
变量和参数是局部的
当你在函数内部创建一个变量时,它是局部的(local),这意味着它只存在于函数内部。例如,以下函数接受两个参数,将它们连接起来,并打印两次结果:
fn cat_twice(part1: str, part2: str) {
let cat = part1 + part2
print_twice(cat)
}下面是一个使用它的例子:
let line1 = "Always look on the "
let line2 = "bright side of life."
cat_twice(line1, line2)当 cat_twice 运行时,它创建一个名为 cat 的局部变量,该变量在函数结束时被销毁。如果我们尝试在函数外部显示它,会得到一个错误——cat 在那里未定义。
参数也是局部的。例如,在 cat_twice 外部,不存在 part1 或 part2。
工作原理
cat_twice 接收两个参数 part1 和 part2,它们是该函数的局部变量。它通过连接两个参数创建了另一个局部变量 cat。当 cat_twice 执行完毕后,它的所有局部变量(cat、part1、part2)都被销毁。
在 main() 中,我们用 line1 和 line2 调用 cat_twice。调用返回后,我们无法从 main() 访问 cat、part1 或 part2——它们只存在于 cat_twice 内部。被注释掉的行展示了如果我们尝试访问会发生什么:在 Python 中会得到 NameError。
栈图
为了追踪哪些变量可以在哪里使用,有时画一个栈图(stack diagram)是很有用的。和状态图一样,栈图显示每个变量的值,但它还显示每个变量属于哪个函数。
每个函数由一个帧(frame)表示。帧是一个方框,外面是函数名,里面是函数的参数和局部变量。
以下是 cat_twice 示例在运行并调用 print_twice 时栈图的文本表示:
+------------------+
| __main__ |
| line1 -> "..." |
| line2 -> "..." |
+------------------+
| cat_twice |
| part1 -> "..." |
| part2 -> "..." |
| cat -> "..." |
+------------------+
| print_twice |
| string -> "..." |
+------------------+帧按栈的方式排列,指示哪个函数调用了哪个函数。从底部向上读,print_twice 被 cat_twice 调用,而 cat_twice 被 __main__ 调用——__main__ 是最顶层帧的特殊名称。当你在任何函数之外创建变量时,它属于 __main__。
回溯
当函数中发生运行时错误时,程序会显示正在运行的函数名、调用它的函数名,依此类推,沿栈向上。这个函数列表被称为回溯(traceback)。
例如,如果 print_twice 尝试访问另一个函数的局部变量,你会得到一个错误。回溯显示 cat_twice 调用了 print_twice,错误发生在 print_twice 中。
回溯中函数的顺序与栈图中帧的顺序相同。发生错误时正在运行的函数位于最底部。
为什么使用函数?
将程序划分为函数的好处可能还不太明显。有以下几个原因:
可读性:创建一个新函数让你有机会为一组语句命名,使程序更容易阅读和调试。
消除重复:函数可以通过消除重复代码来减小程序的体积。以后如果需要修改,只需要在一个地方修改即可。
调试:将长程序划分为函数可以让你逐个部分进行调试,然后将它们组装成一个完整的工作程序。
可重用性:设计良好的函数通常对许多程序都有用。一旦你编写并调试好一个函数,就可以重复使用它。
调试
调试可能令人沮丧,但它也充满挑战、有趣,有时甚至很有成就感。它也是你能学到的最重要的技能之一。
在某些方面,调试就像侦探工作。你得到一些线索,必须推断出导致你所见结果的事件。
调试也像实验科学。一旦你对出了什么问题有了想法,就修改程序并重试。如果你的假设是正确的,你可以预测修改的结果,并向一个可工作的程序迈进一步。如果你的假设是错误的,你必须想出一个新的假设。
对某些人来说,编程和调试是一回事——也就是说,编程是逐步调试程序直到它满足你需求的过程。其理念是,你应该从一个可工作的程序开始,进行小步修改,并在过程中不断调试。
如果你发现自己花了很多时间调试,这通常意味着你在开始测试之前写了太多代码。如果采取更小的步骤,你可能会发现自己能进展得更快。
术语表
函数定义 (function definition): 使用 fn 关键字创建函数的语句。
函数头 (header): 函数定义的第一行,从 fn 到开头的 {。
函数体 (body): 函数定义内部的语句序列,用 {} 包围。
函数对象 (function object): 由函数定义创建的值。函数名是一个指向函数对象的变量。
参数 (parameter): 函数内部使用的名称,用于引用作为参数传递的值。
循环 (loop): 重复执行一个或多个语句的语句。
局部变量 (local variable): 在函数内部定义的变量,只能在函数内部访问。
栈图 (stack diagram): 函数栈、其变量及其指向值的图形表示。
帧 (frame): 栈图中代表函数调用的方框。它包含函数的局部变量和参数。
回溯 (traceback): 发生异常时打印的正在执行的函数列表。
练习
练习
编写一个名为 print_right 的函数,接受一个名为 text 的字符串参数,并打印该字符串,使字符串的最后一个字母位于显示的第 40 列。
提示:使用 len 函数、字符串连接运算符(+)和字符串重复运算符(*)。
练习
编写一个名为 triangle 的函数,接受一个字符串和一个整数,绘制一个使用该字符串副本组成的给定高度的三角形。以下是使用字符串 'L' 的 5 层三角形示例:
L
LLL
LLLLL
LLLLLLL
LLLLLLLLL练习
编写一个名为 rectangle 的函数,接受一个字符串和两个整数,绘制一个使用该字符串副本组成的给定宽度和高度的矩形。例如,rectangle("H", 5, 4) 应该产生:
HHHHH
HHHHH
HHHHH
HHHHH练习
歌曲"99 Bottles of Beer"以这段歌词开始:
99 bottles of beer on the wall 99 bottles of beer Take one down, pass it around 98 bottles of beer on the wall
第二段歌词是一样的,只是从 98 瓶开始,以 97 瓶结束。歌曲一直继续,直到 0 瓶啤酒。
编写一个名为 bottle_verse 的函数,接受一个数字作为参数,显示以给定瓶数开始的那段歌词。
提示:考虑先编写一个可以打印歌词第一行、第二行或最后一行的函数,然后用它来编写 bottle_verse。