函数基础

在本资源各处会有一些阅读理解练习。这些题目旨在帮助读者活学活用文中的知识点。练习题的答案可以在本页底部找到。

定义函数允许我们包装代码片段并描述进入和离开其中的信息。你可以在各种情况下重复使用这个“代码胶囊”。比如说,假设你想要数一个字符串中有多少元音,你可以定义以下的函数来达到该目的:

def count_vowels(in_string):
    """ 返回 `in_string` 中有多少元音 """
    num_vowels = 0
    vowels = "aeiouAEIOU"

    for char in in_string:
        if char in vowels:
            num_vowels += 1  # 等值于 num_vowels = num_vowels + 1
    return num_vowels

执行此代码会定义函数 count_vowels。这个函数期待一个对象作为输入参数(input argument),由 in_string 代名,并会返回(return)该对象中元音的数量。使用 count_vowels 并向其输入一个对象的过程叫做调用(call)该函数:

>>> count_vowels("Hi my name is Ryan")
5

最棒的一点在于你可以重复使用这个函数!

>>> count_vowels("Apple")
2

>>> count_vowels("envelope")
4

在本节中,我们将会了解定义和调用Python函数的语法。

定义

Python函数(function)是一个包装代码的对象。调用(call)函数将会执行包装的代码并返回(return)一个对象。你可以定义函数使其接受参数(argument),也就是输入进包装代码的对象。

def 语句

类似于 ifelse,和 for,Python保留了 def 语句来表达函数(以及我们会在之后讨论的其它几个构造体)的定义。以下是Python函数定义的一般形式:

def <function name>(<function signature>):
    """ 说明字符串 """
    <encapsulated code>
    return <object>
  • <function name> 是任何合法的变量名,在这之后必须跟随一对括号和一个冒号。

  • <function signature> 描述函数的输入参数。如果函数不接受任何变量,你可以留白这一部分(你仍然需要提供括号,但是仅仅括号并不会包装任何参数)。

  • 说明字符串(documentation string)(一般叫做“docstring”)可以长达多行并解释函数的目的。它是可选的。

  • <encapsulated code> 包含正常的Python代码,其由相对于 def 语句的缩进限定。

  • return 当函数中包装的代码遇到这个标示后,函数会返回标示后跟随的对象并立刻终止函数的执行。

Python同样保留了 return 语句来表达函数的终止;如果遇到了 return,那程序将立即终止函数的执行并返回 return 右边的对象。

请注意,与if语句和for循环一样,def 语句必须由冒号结尾,且函数的主体代码必须由空格限定

# 错误的缩进
def bad_func1():
x = 1
    return x + 2

# 错误的缩进
def bad_func2():
    x = 1
return x + 2

# 漏了冒号
def bad_func3()
    x = 1
    return x + 2

# 漏了括号
def bad_func4:
    x = 1
    return x + 2

# 这个没问题
def ok_func():
    x = 1
    return x + 2

阅读理解:编写简单函数

编写一个名为 count_even 的函数。它应该接受一个名为 numbers 的整数可迭代物作为参数。该函数应该返回列表中偶数的数量。注意包含一个合理的docstring。

return 语句

一般来讲,return 语句可以返回任何Python对象。同时,你可以使用一个空的 return 语句或在函数中完全不使用返回(return)语句。在这两种情况下,函数都会返回 ``None`` 对象

# 本函数返回 `None`
# 一个“空”的返回语句
def f():
    x = 1
    return
# 本函数返回 `None`
# 完全没有用返回语句
def f():
    x = 1

所有Python函数都会返回某个对象。就算是内置的 print 函数在打印后也会返回 None

# `print` 函数返回 `None`
>>> x = print("hi")
hi

>>> x is None
True

警告

请注意不要错误地漏掉或使用空的返回语句。你将依然能够调用你的函数,但是它在任何情况下都会返回 None

函数并不一定要有除了返回语句之外的代码。比如说,我们可以使用 sum 和生成器理解(见本模组前一节)来简化我们的 count_vowels 函数:

# 你可以直接描述函数返回的对象
def count_vowels(in_string):
    """ 返回 `in_string` 中元音的数量 """
    return sum(1 for char in in_string if char in "aeiouAEIOU")

多个 return 语句

你可以在一个函数内使用不止一个 return 语句。在处理边缘情况或优化代码时这可能会有用。假设你想要你的函数通过泰勒级数(Taylor series)来模糊计算 \(e^{x}\),当 \(x = 0\) 时函数应该立刻返回 1.0

def compute_exp(x):
    """ 使用泰勒级数来计算 e^x """
    if x == 0:
        return 1.0

    from math import factorial
    return sum(x**n / factorial(n) for n in range(100))

如果 x==0True,那么程序将执行第一个 return 语句,返回 1.0 并立刻“终止”该函数,且不执行之后的代码。

如上所述,就算下方有额外的代码,return 语句也会导致函数立刻终止且不执行之后的代码。在一次函数调用中,程序不可能遇到多个 ``return`` 语句。因此,如果你想要返回多个对象,那么你的函数必须返回单个包含这些项目的容器,如列表会元组。

# 函数返回多个项目
def bad_f(x): # 译者注:函数名字大意为:不好的函数
    """ 返回 x**2 和 x**3"""
    return x**2
    # 此代码永远都不会被执行!
    return x**3

def good_f(x): # 译者注:函数名字大意为:好的函数
    """ return x**2 and x**3"""
    return (x**2, x**3)
>>> bad_f(2)
4

>>> good_f(2)
(4, 8)

单行函数

你可以在一行内定义只有一个返回语句的函数:

def add_2(x):
    return x + 2

可以被重写为:

def add_2(x): return x + 2

尽量只在函数极其简单到不需要docstring就能理解时使用此功能。切记不要滥用。

参数

你可以在函数签名(function signature)部分通过一序列由逗号隔开的变量名来描述函数的位置参数(positional argument)。比如说,以下代码为函数 is_bounded 提供了 xlower,和 upper 三个参数:

def is_bounded(x, lower, upper):
    return lower <= x <= upper

你可以用几种不同的方法来向函数提供参数:

通过位置提供参数

输入到 is_bounded 中的对象会根据它们的位置来赋值函数的输入变量。也就是说,is_bounded(3, 2, 4) 会赋值 x=3lower=2,和 upper=4,其根据函数输入参数的位置顺序来对应:

# 计算:2 <= 3 <= 4
# 通过位置来提供输入
>>> is_bounded(3, 2, 4)
True

向函数输入太少或太多参数会导致 TypeError

# 输入太少:报错
is_bounded(3)

# 输入太多:报错
is_bounded(1, 2, 3, 4)

通过关键词提供参数

在顺序不重要的时候,你也可以在给函数输入参数时提供它们的关键词(也就是名字)来对应对象和参数。这会帮助你编写易读和灵活的代码:

# 计算:2 <= 3 <= 4
# 通过参数名字来提供输入对象
>>> is_bounded(lower=2, x=3, upper=4)
True

你可以混合关键词参数和位置参数,但位置参数应该在前:

# 计算:2 <= 3 <= 4
# `x` 是位置参数
# `lower` 和 `upper` 关键词参数
>>> is_bounded(3, upper=4, lower=2)
True

请注意,如果你提供了一个关键词参数,那么它之后所有的参数都应该是关键词参数:

# 你不可以在关键词参数后使用位置参数
>>> is_bounded(3, lower=2, 4)
SyntaxError: positional argument follows keyword argument

有默认值的参数

你可以提供参数的默认值。如果用户在调用函数时没有提供此参数,那么函数将会使用定义的默认值。请回忆我们的 count_vowels 函数。假设我们想要提供将“y”算为一个元音的选择。因为我们知道人们一般不会将“y”视为元音,所以我们可以默认将“y”除外:

def count_vowels(in_string, include_y=False):
    """ 返回 `in_string` 中元音的数量"""
    vowels = "aeiouAEIOU"
    if include_y:
        vowels += "yY"  # 将 "y" 加到元音列表中
    return sum(1 for char in in_string if char in vowels)

现在,如果在调用 count_vowels 时只提供了 in_string,那么 include_y 会使用默认值 False

# 使用默认值:不将y算为元音
>>> count_vowels("Happy")
1

我们可以提供默认值之外的输入:

# 不使用不认值:将y算为元音
>>> count_vowels("Happy", True)
2

# 你依然可以通过名字来描述输入参数
>>> count_vowels(include_y=True, in_string="Happy")
2

在函数签名中,有默认值的输入参数必须在所有位置参数之后:

# 这没问题
def f(x, y, z, count=1, upper=2):
    return None
# 这会导致语法错误
def f(x, y, count=1, upper=2, z):
    return None

阅读理解:函数和参数

编写一个函数 max_or_min。它接受两个位置参数 xy(将会被赋值为数字)以及一个 mode 变量,其默认值为 "max"

此函数应该根据 mode 返回 min(x, y)max(x, y)。如果 mode 既不是 "max" 也不是 "min" 的时候让函数返回 None

包含一个描述性的docstring。

支持任意多的位置参数

Python提供了定义可以接受任意多位置参数的函数的语法。使用 def f(*<var_name>) 语法来定义这类输入。

# * 符号表明调用 `f` 时可以向 `args`
# 输入任意多的参数。
def f(*args):
    #  所有向 `f` 输入的参数都会被“打包”成一个元组
    #  并赋值给变量 `args`。
    # `f()` 会赋值 `args = tuple()`
    # `f(x, y, ...)` 会赋值 `args = (x, y, ...)`
    return args

因为Python不能提前知道 f 会收到多少个参数,因此它所有的输入参数都会被打包成一个元组并赋值给变量 args

# 向 `f` 输入0个参数
>>> f()
()

# 向 `f` 输入1个参数
>>> f(1)
(1,)

# 向 `f` 输入3个参数
>>> f((0, 1), True, "cow")
((0, 1), True, "cow")

你可以将此语法与位置参数和默认参数混合使用。任何在被打包的变量后的变量必须提供参数名

def f(x, *seq, y):
    print("x is: ", x)
    print("seq is: ", seq)
    print("y is: ", y)
    return None
>>> f(1, 2, 3, 4, y=5)  # 必须提供 `y` 的名字
x   是: 1
seq 是: (2, 3, 4)
y   是: 5
>>> f("cat", y="dog")  # 没有输入任何额外的位置参数
x   是: "cat"
seq 是: ()
y   是: "dog"

阅读理解:任意多的参数

编写一个名为 mean 的函数,其接受任意多的数字参数并计算所有数字的平均值。如,mean(1, 2, 3) 应返回 \(\frac{1 + 2 + 3}{3} = 2.0\)

如果没有收到任何输入,函数应返回 0.。记得测试你的函数并编写一个docstring。

如我们所见,在函数定义的签名中使用 * 意味着将任意多的参数打包成一个元组。同时,在调用函数时 * 也意味着解包可迭代物并将其作为位置参数输入到函数中:

# 在调用函数时使用 `*` 来解包可迭代物,并将其
# 成员作为位置参数输入到函数中

def f(x, y, z):
    return x + y + z

>>> f(1, 2, 3)
6

# `*` 意味着:解包 `[1, 2, 3]` 的内容并将每个
# 物件分别输入为x,y,和z
>>> f(*[1, 2, 3])  # 等值于:f(1, 2, 3)
6

在以下范例中,我们使用 * 来:

  1. 定义一个接受任意多参数并将其打包为元组的函数

  2. 通过解包可迭代物来调用函数并输入任意多的参数

def number_of_args(*args):
    return len(args)
>>> number_of_args(None, None, None, None)
4

>>> some_list = [1, 2, 3, 4, 5]

# 将列表本身作为唯一的参数输入
>>> number_of_args(some_list)
1

# 将列表的5个成员解包并作为多个参数输入进函数
>>> number_of_args(*some_list)
5

支持任意多的关键词参数

我们可以使用 def f(**<var_name>) 语法来定义一个接受任意多的关键词(keyword)参数的函数。

注意单个星号 * 用来代表任意多的位置参数,而 ** 代表着任意多的关键词参数。

# ** 符号意味着在调用 `f` 时可以向 `args` 输入
# 任意多的关键词参数。
def f(**args):
    # 所有输入到 `f` 的关键词参数都会被“打包”成一个词典
    # 并赋值到变量 `args` 中
    # `f()` 会赋值 `args = {}`(一个空词典)
    # `f(x=1, y=2, ...)` 会赋值 `args = {"x":1, "y":2, ...}`
    return args

因为Python无法预先知道 f 会收到多少个关键词参数,所以所有收到的关键词参数都会被打包成一个词典(dictionary)。词典允许你通过关键词(将其转化成字符串)查询并设置关键词对应的值。这个词典被赋值到变量 args 上。我们会在后面一节专门讨论词典。

>>> f()            # 向 `f` 输入0个参数
{}

>>> f(x=1)           # 向 `f` 输入1个参数
{'x': 1}

>>> f(x=(0, 1), val=True, moo="cow")  # 向 `f` 输入3个参数
{'moo': 'cow', 'val': True, 'x': (0, 1)}

这一语法可以和位置参数和默认参数混合使用。在函数定义签名中,** 后不能有任何额外的参数:

def f(x, y=2, **kwargs):
    print("x 是:", x)
    print("y 是:", y)
    print("kwargs 是:", kwargs)
    return None

# 译者注:kwargs是keyword arguments的缩写,也就是关键词参数。
# 译者注:此命名方式是传统,所以请尽量遵循
# 向 `f` 输入任意的关键词参数
>>> f(1, y=9, z=3, k="hi")
x 是: 1
y 是: 9
kwargs 是: {'z': 3, 'k': 'hi'}
# 没有输入任何额外的关键词参数
>>> f("cat", y="dog")
x is:  cat
y is:  dog
kwargs is:  {}

以下函数接受任意多的位置参数任意多的关键词参数:

# 接受任意多的位置和关键词参数
def f(*x, **y):
    # 所有位置参数都打包成元组 `x`
    # 所有关键词参数都打包成词典 `y`
    print(x)
    print(y)
    return None

>>> f(1, 2, 3, hi=-1, bye=-2, sigh=-3)
(1, 2, 3)
{'hi': -1, 'bye': -2, 'sigh': -3}

如上所见,在函数定义的签名中 ** 意味着将任意多的关键词参数打包成一个词典。同时,在调用函数时 ** 意味着解包词典并将其的键值对(key-value pair)作为函数关键词参数输入:

# 在调用函数时使用 `**` 来解包词典并将其成员作为
# 关键词参数输入到函数中
def f(x, y, z):
    return 0*x + 1*y + 2*z

>>> f(z=10, x=9, y=1)
21

>>> args = {"x": 9, "y": 1, "z": 10}
>>> f(**args)  # 等值于:f(x=9, y=1, z=10)
21

在以下范例中,我们用 ** 来:

  1. 定义一个接受任意多关键词参数的函数并将其打包成词典。

  2. 调用函数并通过解包词典向其输入任意多的关键词参数。

def print_kwargs(**args):
    print(args)
>>> print_kwargs(a=1, b=2, c=3, d=4)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

>>> some_dict = {"hi":1, "bye":2}

# 解包词典的键值对并将其作为关键词参数输入到函数中
>>> print_kwargs(a=2, umbrella=True, **some_dict)
{'a': 2, 'umbrella': True, 'hi': 1, 'bye': 2}

函数也是对象

在定义之后,函数和任何其它Python对象,如列表或字符串或整数,的行为差不多。你可以将函数赋值到变量上:

>>> var = count_vowels  # `var` 现在引用函数 `count_vowels`
>>> var("Hello")        # 你现在可以“调用” `var`
2

你可以将函数存储到一个列表中:

my_list = [count_vowels, print]

for func in my_list:
    func("hello")

# 迭代0:调用 `count_vowels("hello")`
# 迭代1:调用 `print("hello")`

你也可以在代码任何地方调用函数,且它的返回值会在原地返回:

if count_vowels("pillow") > 1:
    print("that's a lot of vowels!")

当然,这在列表理解表达式中也成立:

>>> sum(count_vowels(word, include_y=True) for word in ["hi", "bye", "guy", "sigh"])
6

“打印”一个函数并不会揭露些什么。这仅仅打印此函数对象在内存中的地址:

>>> print(count_vowels)
<function count_vowels at 0x000002A32898C6A8>

阅读理解答案

编写简单函数:解

def count_even(numbers):
    """ 返回可迭代物中偶数的数量 """
    total_even = 0
    for num in numbers:
        if num % 2 == 0:
            total += 1
    return total

或通过使用生成器理解:

def count_even(numbers):
    """ 返回可迭代物中偶数的数量 """
    return sum(1 for num in numbers if num % 2 == 0)

函数和参数:解

def max_or_min(x, y, mode="max"):
    """ 根据 `mode` 参数返回 `max(x,y)` 或 `min(x,y)`。

        Parameters
        ----------
        x : Number

        y : Number

        mode : str
            'max' 或 'min'

        Returns
        -------
        两值的最大或最小值。如果mode不合法那么会返回 `None`。"""
    if mode == "max":
        return max(x, y)
    elif mode == "min":
        return min(x, y)
    else:
        return None

请注意你其实可以在 mode 输入不正确时让你的函数报错(raise an “exception”)。事实上,这才应该是这种情况下更加合理的函数行为。

这种解决方案如下:

def max_or_min(x, y, mode="max"):
    if mode == "max":
        return max(x, y)
    elif mode == "min":
        return min(x, y)
    else:
        raise Exception("`mode` was passed an invalid value: {}".format(mode))

任意多的参数:解

def mean(*seq):
    """ 返回函数参数的平均值 """
    if len(seq) == 0:
        return 0

    total = 0
    for num in seq:
        total += num
    return total / len(seq)

或者我们可以利用以下两点来做一些骚操作:

  • seq 为空时 bool(seq)False 的事实

  • 单行if-else语法

def mean(*seq):
    """ 返回函数参数的平均值 """
    return sum(seq) / len(seq) if seq else 0