Skip to main content

函数

函数调用

在编程中,函数 (function) 是执行特定计算的一段命名的语句序列。当你定义一个函数时,你需要指定函数名和语句序列。之后,你可以通过函数名来“调用”它。我们已经见过一个函数调用 (function call) 的例子:

>>> type(32)
<class 'int'>

这个函数的名字是 type。括号中的表达式称为函数的实参 (argument)。实参是你传递给函数作为输入的值或变量。对于 type 函数来说,其结果就是实参的类型。

通常我们说函数“接受”一个实参并“返回”一个结果。这个结果被称为返回值 (return value)。

内置函数

Python 提供了许多重要的内置函数,我们无需提供函数定义即可使用。Python 的创建者编写了一系列函数来解决常见问题,并将它们包含在 Python 中供我们使用。

maxmin 函数分别给出列表中的最大值和最小值:

>>> max('Hello world')
'w'
>>> min('Hello world')
' '
>>>

max 函数告诉我们字符串中的“最大字符”(结果是字母“w”),而 min 函数向我们展示了最小字符(结果是一个空格)。

另一个非常常见的内置函数是 len 函数,它告诉我们其参数中有多少项。如果 len 的参数是一个字符串,它返回字符串中的字符数。

>>> len('Hello world')
11
>>>

这些函数不仅限于处理字符串。它们可以操作任何值集合,我们将在后面的章节中看到。

你应该将内置函数的名称视为保留字(即,避免使用“max”作为变量名)。

类型转换函数

Python 还提供了将值从一种类型转换为另一种类型的内置函数。int 函数接受任何值,并尽可能将其转换为整数,否则会报错:

>>> int('32')
32
>>> int('Hello')
ValueError: invalid literal for int() with base 10: 'Hello'

int 可以将浮点值转换为整数,但它不会四舍五入;它会截断小数部分:

>>> int(3.99999)
3
>>> int(-2.3)
-2

float 将整数和字符串转换为浮点数:

>>> float(32)
32.0
>>> float('3.14159')
3.14159

最后,str 将其参数转换为字符串:

>>> str(32)
'32'
>>> str(3.14159)
'3.14159'

数学函数

Python 有一个 math 模块,提供了大多数常见的数学函数。在使用该模块之前,我们必须导入它:

>>> import math

这条语句创建了一个名为 math 的模块对象 (module object)。如果你打印这个模块对象,你会得到一些关于它的信息:

>>> print(math)
<module 'math' (built-in)>

模块对象包含模块中定义的函数和变量。要访问其中一个函数,你必须指定模块名称和函数名称,用点(也称为句点)分隔。这种格式称为点表示法 (dot notation)。

>>> ratio = signal_power / noise_power
>>> decibels = 10 * math.log10(ratio)

>>> radians = 0.7
>>> height = math.sin(radians)

第一个例子计算信噪比的以 10 为底的对数。math 模块还提供了一个名为 log 的函数,用于计算以 e 为底的对数。

第二个例子计算 radians 的正弦值。变量名提示 sin 和其他三角函数(costan 等)接受以弧度为单位的参数。要从度转换为弧度,需除以 360 再乘以 2 π

>>> degrees = 45
>>> radians = degrees / 360.0 * 2 * math.pi
>>> math.sin(radians)
0.7071067811865476

表达式 math.pi 从 math 模块中获取变量 pi。该变量的值是 π 的近似值,精确到约 15 位数字。

如果你懂三角学,可以通过将其与 2 的平方根除以 2 的结果进行比较来验证前面的结果:

>>> math.sqrt(2) / 2.0
0.7071067811865476

随机数

给定相同的输入,大多数计算机程序每次都会生成相同的输出,因此它们被称为确定性的 (deterministic)。确定性通常是件好事,因为我们期望相同的计算产生相同的结果。然而,对于某些应用,我们希望计算机是不可预测的。游戏是一个明显的例子,但还有更多。

使程序真正具有非确定性并不容易,但有方法可以使其至少看起来具有非确定性。其中之一是使用生成伪随机数 (pseudorandom numbers) 的算法 (algorithms)。伪随机数并非真正随机,因为它们是由确定性计算生成的,但仅通过观察这些数字,几乎不可能将它们与真随机数区分开来。

random 模块提供了生成伪随机数(从现在起我简称为“随机数”)的函数。

函数 random 返回一个介于 0.0 和 1.0 之间(包括 0.0 但不包括 1.0)的随机浮点数。每次调用 random,你都会得到一个长序列中的下一个数字。要查看示例,请运行此循环:

import random

for i in range(10):
x = random.random()
print(x)

此程序产生以下 10 个随机数的列表,这些数介于 0.0 和(但不包括)1.0 之间。

0.11132867921152356
0.5950949227890241
0.04820265884996877
0.841003109276478
0.997914947094958
0.04842330803368111
0.7416295948208405
0.510535245390327
0.27447040171978143
0.028511805472785867

练习 1: 在你的系统上运行该程序,看看你会得到什么数字。多次运行该程序,看看你会得到什么数字。

random 函数只是处理随机数的众多函数之一。函数 randint 接受参数 lowhigh,并返回一个介于 lowhigh 之间(包括两者)的整数。

>>> random.randint(5, 10)
5
>>> random.randint(5, 10)
9

要从序列中随机选择一个元素,你可以使用 choice

>>> t = [1, 2, 3]
>>> random.choice(t)
2
>>> random.choice(t)
3

random 模块还提供函数来从连续分布(包括高斯分布、指数分布、伽马分布等)生成随机值。

添加新函数

到目前为止,我们只使用了 Python 自带的函数,但也可以添加新函数。函数定义 (function definition) 指定了新函数的名称以及调用该函数时执行的语句序列。一旦我们定义了一个函数,我们就可以在整个程序中反复重用该函数。

下面是一个例子:

def print_lyrics():
print("I'm a lumberjack, and I'm okay.")
print('I sleep all night and I work all day.')

def 是一个关键字,表示这是一个函数定义。函数的名字是 print_lyrics。函数名的规则与变量名相同:字母、数字和一些标点符号是合法的,但第一个字符不能是数字。你不能使用关键字作为函数名,并且应该避免变量和函数同名。

名称后面的空括号表示此函数不接受任何参数。稍后我们将构建接受参数作为输入的函数。

函数定义的第一行称为头部 (header);其余部分称为主体 (body)。头部必须以冒号结尾,主体必须缩进。按照惯例,缩进总是四个空格。主体可以包含任意数量的语句。

如果你在交互模式下键入函数定义,解释器会打印省略号 (...) 来告诉你定义尚未完成:

>>> def print_lyrics():
... print("I'm a lumberjack, and I'm okay.")
... print('I sleep all night and I work all day.')
...

要结束函数定义,你必须输入一个空行(在脚本中则不需要)。

定义函数会创建一个同名的变量。

>>> print(print_lyrics)
<function print_lyrics at 0xb7e99e9c>
>>> print(type(print_lyrics))
<class 'function'>

print_lyrics 的值是一个函数对象 (function object),其类型为“function”。

调用新函数的语法与调用内置函数的语法相同:

>>> print_lyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.

一旦你定义了一个函数,你就可以在另一个函数内部使用它。例如,要重复前面的副歌,我们可以编写一个名为 repeat_lyrics 的函数:

def repeat_lyrics():
print_lyrics()
print_lyrics()

然后调用 repeat_lyrics

>>> repeat_lyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.

但这并不是那首歌真正的唱法。

定义和使用

将上一节的代码片段整合在一起,整个程序看起来像这样:

def print_lyrics():
print("I'm a lumberjack, and I'm okay.")
print('I sleep all night and I work all day.')

def repeat_lyrics():
print_lyrics()
print_lyrics()

repeat_lyrics()

# 代码: https://www.py4e.com/code3/lyrics.py

这个程序有两个函数定义:print_lyricsrepeat_lyrics。函数定义像其他语句一样被执行,但其效果是创建函数对象。函数内部的语句在函数被调用之前不会执行,函数定义本身不产生任何输出。

正如你可能预料到的,你必须先创建函数才能执行它。换句话说,函数定义必须在它第一次被调用之前执行。

练习 2: 将此程序的最后一行移到顶部,使函数调用出现在定义之前。运行程序,看看你会得到什么错误消息。

练习 3: 将函数调用移回底部,并将 print_lyrics 的定义移到 repeat_lyrics 的定义之后。运行此程序时会发生什么?

执行流程

为了确保函数在其首次使用前被定义,你必须了解语句执行的顺序,这被称为执行流程 (flow of execution)。

执行总是从程序的第一个语句开始。语句按从上到下的顺序逐一执行。

函数定义不会改变程序的执行流程,但请记住,函数内部的语句在函数被调用之前不会执行。

函数调用就像执行流程中的一次绕行。流程不是转到下一条语句,而是跳转到函数的主体,执行那里的所有语句,然后返回到它离开的地方继续执行。

这听起来足够简单,直到你记起一个函数可以调用另一个函数。在执行一个函数的过程中,程序可能需要执行另一个函数中的语句。但是在执行那个新函数时,程序可能还需要执行再另一个函数!

幸运的是,Python 擅长跟踪自己所在的位置,所以每次函数完成时,程序都会在调用它的函数中从它离开的地方继续。当程序到达末尾时,它会终止。

这个有点复杂的故事告诉我们什么道理?当你阅读一个程序时,你不总是想从上到下阅读。有时,如果你遵循执行流程,会更有意义。

形参和实参

我们已经看到的一些内置函数需要实参。例如,当你调用 math.sin 时,你传递一个数字作为实参。有些函数接受多个实参:math.pow 接受两个,底数和指数。

在函数内部,实参被赋给称为形参 (parameters) 的变量。下面是一个接受实参的用户定义函数的例子:

def print_twice(bruce):
print(bruce)
print(bruce)

这个函数将实参赋给名为 bruce 的形参。当函数被调用时,它会打印形参的值(无论它是什么)两次。

这个函数适用于任何可以打印的值。

>>> print_twice('Spam')
Spam
Spam
>>> print_twice(17)
17
17
>>> import math
>>> print_twice(math.pi)
3.141592653589793
3.141592653589793

适用于内置函数的组合规则也适用于用户定义的函数,因此我们可以使用任何类型的表达式作为 print_twice 的实参:

>>> print_twice('Spam '*4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> print_twice(math.cos(math.pi))
-1.0
-1.0

实参在函数被调用之前被求值,因此在示例中,表达式 'Spam '*4math.cos(math.pi) 都只被求值一次。

你也可以使用变量作为实参:

>>> michael = 'Eric, the half a bee.'
>>> print_twice(michael)
Eric, the half a bee.
Eric, the half a bee.

我们作为实参传递的变量名称 (michael) 与形参的名称 (bruce) 没有任何关系。无论这个值在“家”(调用者中)被称为什么;在 print_twice 函数里,我们都叫它 bruce

有返回值函数和无返回值函数

我们正在使用的一些函数,例如数学函数,会产生结果;因为没有更好的名字,我称它们为有返回值的函数 (fruitful functions)。其他函数,如 print_twice,执行一个动作但不返回值。它们被称为无返回值的函数 (void functions)。

当你调用一个有返回值的函数时,你几乎总是想对结果做些什么;例如,你可能将其赋给一个变量或将其用作表达式的一部分:

x = math.cos(radians)
golden = (math.sqrt(5) + 1) / 2

当你在交互模式下调用函数时,Python 会显示结果:

>>> math.sqrt(5)
2.23606797749979

但在脚本中,如果你调用一个有返回值的函数并且不将函数的结果存储在变量中,返回值就会消失得无影无踪!

math.sqrt(5)

这个脚本计算 5 的平方根,但由于它没有将结果存储在变量中或显示结果,所以它不是很有用。

无返回值的函数可能会在屏幕上显示某些内容或产生其他效果,但它们没有返回值。如果你尝试将结果赋给一个变量,你会得到一个特殊的值,称为 None

>>> result = print_twice('Bing')
Bing
Bing
>>> print(result)
None

None 值与字符串 “None” 不同。它是一个特殊的值,有自己的类型:

>>> print(type(None))
<class 'NoneType'>

要从函数返回结果,我们在函数中使用 return 语句。例如,我们可以创建一个非常简单的函数 addtwo,它将两个数字相加并返回结果。

def addtwo(a, b):
added = a + b
return added

x = addtwo(3, 5)
print(x)

# 代码: https://www.py4e.com/code3/addtwo.py

当这个脚本执行时,print 语句将打印出 “8”,因为 addtwo 函数被调用时传入了 3 和 5 作为实参。在函数内部,形参 ab 分别是 3 和 5。该函数计算了两个数字的和,并将其放入名为 added 的局部函数变量中。然后它使用 return 语句将计算出的值作为函数结果发送回调用代码,该结果被赋给变量 x 并打印出来。

为什么使用函数?

将程序划分为函数是否值得这么麻烦,可能还不清楚。有以下几个原因:

  • 创建一个新函数让你有机会为一组语句命名,这使得你的程序更易于阅读、理解和调试。

  • 函数可以通过消除重复代码使程序更小。以后,如果你需要做修改,你只需要在一个地方修改。

  • 将一个长程序划分为函数允许你一次调试一个部分,然后将它们组装成一个可工作的整体。

  • 精心设计的函数通常对许多程序都有用。一旦你编写并调试好一个,你就可以重用它。

在本书的其余部分,我们经常会使用函数定义来解释一个概念。创建和使用函数的技能之一是让函数恰当地捕捉一个想法,例如“在值列表中找到最小值”。稍后我们将向你展示在值列表中找到最小值的代码,并将其作为名为 min 的函数呈现给你,该函数接受一个值列表作为其参数,并返回列表中的最小值。

调试

如果你使用文本编辑器编写脚本,你可能会遇到空格和制表符的问题。避免这些问题的最好方法是只使用空格(不使用制表符)。大多数了解 Python 的文本编辑器默认会这样做,但有些不会。

制表符和空格通常是不可见的,这使得它们难以调试,所以尽量找一个能为你管理缩进的编辑器。

另外,不要忘记在运行程序之前保存它。一些开发环境会自动执行此操作,但有些不会。在这种情况下,你在文本编辑器中看到的程序与你正在运行的程序不同。

如果你一遍又一遍地运行同一个不正确的程序,调试可能会花费很长时间!

确保你正在查看的代码就是你正在运行的代码。如果不确定,在程序的开头加上类似 print("hello") 的语句再运行一次。如果你没有看到 hello,那么你运行的就不是正确的程序!

术语表

算法 (algorithm) 解决一类问题的通用过程。 实参 (argument) 调用函数时提供给函数的值。此值被赋给函数中相应的形参。 主体 (body) 函数定义内部的语句序列。 组合 (composition) 将表达式用作更大表达式的一部分,或将语句用作更大语句的一部分。 确定性的 (deterministic) 指程序在给定相同输入的情况下,每次运行时都执行相同的操作。 点表示法 (dot notation) 通过指定模块名称后跟一个点(句点)和函数名称来调用另一个模块中的函数的语法。 执行流程 (flow of execution) 程序运行时语句执行的顺序。 有返回值的函数 (fruitful function) 返回一个值的函数。 函数 (function) 执行某些有用操作的命名语句序列。函数可能接受或不接受实参,也可能产生或不产生结果。 函数调用 (function call) 执行函数的语句。它由函数名后跟一个实参列表组成。 函数定义 (function definition) 创建一个新函数的语句,指定其名称、形参以及它执行的语句。 函数对象 (function object) 由函数定义创建的值。函数名是一个引用函数对象的变量。 头部 (header) 函数定义的第一行。 导入语句 (import statement) 读取模块文件并创建模块对象的语句。 模块对象 (module object) 由 import 语句创建的值,提供对模块中定义的数据和代码的访问。 形参 (parameter) 在函数内部用于引用作为实参传递的值的名称。 伪随机 (pseudorandom) 指一个数字序列看起来是随机的,但实际上是由确定性程序生成的。 返回值 (return value) 函数的结果。如果函数调用被用作表达式,则返回值就是该表达式的值。 无返回值的函数 (void function) 不返回值的函数。

练习

练习 4: Python 中 “def” 关键字的用途是什么?

a) 它是俚语,意思是“接下来的代码非常酷” b) 它表示一个函数的开始 c) 它表示接下来的缩进代码段将被存储起来以备后用 d) b 和 c 都正确 e) 以上都不是

练习 5: 以下 Python 程序将打印出什么?

def fred():
print("Zap")

def jane():
print("ABC")

jane()
fred()
jane()

a) Zap ABC jane fred jane b) Zap ABC Zap c) ABC Zap jane d) ABC Zap ABC e) Zap Zap Zap

练习 6: 重写你的工资计算程序,对加班时间(超过40小时)支付 1.5 倍的工资,并创建一个名为 computepay 的函数,该函数接受两个参数(hoursrate)。

Enter Hours: 45
Enter Rate: 10
Pay: 475.0

练习 7: 使用一个名为 computegrade 的函数重写上一章的成绩程序,该函数接受一个分数作为其参数,并返回一个表示等级的字符串。

 Score   Grade
>= 0.9 A
>= 0.8 B
>= 0.7 C
>= 0.6 D
< 0.6 F
Enter score: 0.95
A
Enter score: perfect
Bad score
Enter score: 10.0
Bad score
Enter score: 0.75
C
Enter score: 0.5
F

重复运行程序以测试各种不同的输入值。


如果你在本书中发现错误,欢迎使用 Github 给我发送修正。