10: 定义

在这节课中,我们会向你展示编程中最重要的内容:定义函数!函数让你的程序变得更短, 更整洁, 便于阅览和调试,提高可重复使用性。在后面的课程中,我们会见到其他的好处,例如递归

定义一个函数

我们会从一个例子开始。还记得abs这个函数么? 它读取一个实参(一个数字 x),然后返还它的绝对值(当x ≥ 0,等于x;当 x < 0,等于-x)。在Python中,它是这样定义这个函数的:

def abs(x):     # 定义一个带有一个实参"x"的函数“abs”
  if x >= 0:    # 函数从这里开始
    return x
  else:
    return -x   # 函数在这里结束
(点击这里可以在控制台中测试这段代码。)

函数定义的两个部分

函数定义的第一行确定了这个函数的名字实参。它应该看上去像这样:

def «函数名字»(«实参列»):

在上面的例子中, abs是函数的名字,并且实参列是x。之所以称之为实参列是因为这里可以有多个实参,比如x, y, z; 同样,没有实参也是可行的, 例如一个空的列。输入可以作为一个函数的实参。

所有在函数定义第一行之后的内容都是函数体。函数体是一块缩进的代码。每当一个函数被调用, 函数体会在这些实参或输入中执行任务。最终, 当我们到达函数体中的这行,

return «值»

这个函数就会停止运行并返还«值»作为它的输出。我们可以理解为, 返还的值是这个函数的输出。 在 abs 这个例子中,函数体中含有多个return 语句;但只有第一个被执行的会产生作用,因为这个函数会在执行第一个后停止。

示例: 定义和调用一个函数

下面的这个程序含有一个函数定义和一些会在这个函数被定义后执行的语句。你能猜到输出会是什么吗?

我们看到,函数的名字是 square, 它只有一个实参 x, 它的函数体也只有一行:return x*x。在函数的外面我们有两个指令, 他们总共调用了这个函数三次。

可视化窗口向我们展示了一个独立的工作空间(一)在每一次函数被调用时都会被分配。当函数返还时,这个工作空间就不再需要并被清除了。

这里是每一步的解释:

  1. 当第一个指令被执行, Python必须运算square(10).
    • Python会将输入列(10)和实参列(x)进行匹配。所以,它会执行函数体return x*x。同时需要记住的是 x等于10。所以x*x会产生100,并将其打印。
  2. 当第二个指令被执行, Python必须运算square(square(2)).
    • 内部部分square(2)是先被运算的。我们暂时先设x等于2,并运行函数体。它会返还4
    • 接着外部的表达式会被运算; 因为 square(2) 会给出 4, 我们现在会调用 square(4). 它会返还 16, 并将其打印。

四个常见错误

一个在定义函数中的常见错误就是忘记return语句。

示例
错误1:忘记return

如果函数体中没有return语句,那么函数会默认给出None。所以,如果你的一些练习因为函数返还None而没有成功, 那么问题通常是某些函数输入并没有使return语句被执行。

有事也可能会特意忽略return语句。这种情况只有当你的函数有一些除了返回值外的副作用才可行,例如打印一些输出:
示例: 有一个副作用并且没有return语句的示例

另一个常见的错误是忘记缩进代码, 会造成IndentationError错误。

示例
错误2: 忘记缩进

就像我们在第2课中看到的,调用函数时过多或过少的实参都会造成错误。

示例
错误3: 调用时过多实参

最后,如果你在定义或者调用函数时打错字,你会得到一个错误,说这个函数未定义。

示例
错误4: 错误名字

自己尝试

编程练习: Cubism
定义一个函数cube(n),把单独一个数字n 作为输入,并输出它的立方n × n × n.

两个或多个实参

上面的这些函数都只有一个实参, 但实际上一个函数可以有任意数量的实参。例如,你曾经调用过的input()并不需要任何实参(你会在15A中定义没有实参的函数)。

这里是一个有两个实参的例子,它和几何有关。假设我们需要一个函数来计算三角形的面积,它的底和高的长度是已知的。计算面积的公式是

面积 = 底 × 高 / 2

在代码中, 它看上去向下面这样:我们用def «函数名»(«实参1», «实参2»)替换def «函数名»(«实参1»)

注意,当一个函数被定义和被执行是不同的。函数体只有当这个函数被调用时才会执行。请完成下面这个联系来掌握这个知识点:

选择题练习: In-n-Out
下面这个程序的输出是什么?

def f():          # 0个实参的函数
   print("in f")
print("not in f")
f()               # 调用f
正确! 函数f是已经被定义的,但它的函数体并没有被立即执行。当这个函数被定义后,我们打印出not in f。然后这个函数才被调用,并且它的函数体被执行,打印 in f

在上个练习中,我们要求你写了一个有两个实参的函数,来计算长方形的周长(周长是所有边长度的总和)。当长和宽已知时,周长的公式是:

\textrm{perimeter = width + height + width + height}

请参照右边的例图。



编程练习: Rectangle
定义一个函数rectanglePerimeter(width, height),来计算长方形的周长。

函数调用函数

函数是建立良好的大型程序的基石:你可以避免重复写同样的代码来完成同样的任务,你也可以重复使用你的答案来完成常见的任务。下面是一个例子,建立一个函数并在另一个函数中调用它。

在你运行这个程序之前你可以猜到这个程序的作用是什么吗?在第4课中,我们学到用字符串乘以一个整数意味着重复这个字符串相应的倍数。例如,"tar"*2"tartar"

向左或向右拖动垂直的灰线来改变可视化和代码显示的多少。

在可视化中你可以看到一个新的现象: 在"帧"这列下,当我们在执行30步中的第八步时,这里不只有一些全局变量,同样也有两个帧(一个是为了 outer,另一个是为了刚建立的inner)。这个outer会暂停并等待inner完全结束,然后 outer会重新开始。接着它会再调用一遍inner,同样outer还是等待着;当inner完成时,outer会接着完成。最终,我们调用outer的整个过程又会接着重复。

你现在可以进入下一课了!