Logo
Section 1 / 4

返回相关提示

学习如何更高效地使用 Python 中的 return 语句。

检查点

返回相关提示

学习如何更高效地使用 Python 中的 return 语句。

函数 是一种计算单元。它是为特定操作而设计和优化的工具,可以被我们自己以及其它程序员重复使用。优秀的函数能够提升程序的模块化程度、清晰度和可维护性,从而整体提高程序的价值。我们的程序的有效性取决于其函数的质量。

在许多方面,Python 函数与其他流行语言中的函数相似:它们接收参数,执行计算,并将计算结果返回给调用者,尽管也存在一些差异。

任何超过十几行代码的程序都必须使用函数。

在本章中,我们将学习 Python 中特有的函数设计机制:

  • 多重返回
  • 可选参数和关键字参数
  • 生成器
  • 匿名函数
  • 函数生成函数

始终让函数返回一个值

C、C++、Java 甚至 Fortran 的函数要么只能返回一个结果,要么不返回任何值(即 void 函数),而 Python 要求每个函数必须恰好返回一个值。
如果函数末尾没有 return 语句,或者 return 语句后没有值,函数将返回 None。例如:

1def aFuncThatReturnsNone(): 2 a = 1 3aFuncThatReturnsNone() # 无输出 4print(aFuncThatReturnsNone())

如果函数中有带一个值的 return 语句,该值就会被返回。


保持返回值的一致性

遵循 Python 风格的做法是确保函数始终返回相同类型的值。

由于 Python 没有函数原型,因此可以设计出在相同参数类型下返回不同类型数据的函数。以正则表达式匹配函数 re.search() 为例:成功时返回 _sre.SRE_Match 对象,失败时返回 None:

1print(re.search('0', 'hello')) # 无输出 2print(re.search('o', 'hello'))

幸运的是,None 的布尔值为 False,因此我们可以判断是否可以继续处理返回的对象:

1match = re.search(pattern, 'hello') 2if match: 3 # 对 match 对象进行操作

但更糟糕的情况是:函数成功时返回非字符串对象,失败时返回错误信息字符串:

1def make_list(n): 2 if n >= 0: 3 return list(range(n)) 4 else: 5 return 'The value of n is negative' 6print(make_list(5)) 7print(make_list(-1))

在这种情况下,有效结果有时逻辑上为假(当 n == 0 时),而无效结果总是逻辑上为真。唯一能区分成功与失败的方法是显式检查返回值的类型。

我们可以采用一种符合 Python 风格的技术来保证返回值一致性。如果我们假设有效结果和失败情况出现频率相当,可以选择一个与有效结果同类型但永远无效的“哨兵”值。例如,若函数应返回正数,则可用负数表示失败。str.find() 就采用了这种方法:

1print('abc'.find('a')) 2print('abc'.find('z')) # 未找到!

如果我们无法选择合适的哨兵值,或确定失败情况很少发生,就应通过抛出异常来报告错误。

str.index() 就使用了这种策略:

1print('abc'.index('a')) 2print('abc'.index('z')) # 未找到!

两种方式都可以接受,但如果同一个类中的两个方法功能非常相似,却使用不同的失败报告机制,就不够 Pythonic。


返回多个值

如果希望函数返回多个值,比如用逗号分隔的几个值,可以尝试如下写法:

1def aFuncThatReturnsValues(): 2 return 1, 2, 3, 4 # 真的返回四个值吗? 3 4result = aFuncThatReturnsValues() 5print(result)

输出看起来像一个元组。

我们再运行以下代码确认返回值的类型:

1print(type(result))

结果确实是元组。实际上,一个试图返回多个值的函数,真正返回的是一个对象:由这些值组成的元组。通过逗号分隔序列构建元组的过程称为 打包(packing)。当函数看似返回多个值时,它会将这些值打包成一个元组并返回。

结论:函数始终只返回一个结果。


省略 else 后的 return

return 语句会终止当前函数的执行。即使 return 不是函数的最后一行,后续代码也不会被执行。我们可以利用这一特性来简化复杂的条件判断。

例如,若某个条件为真时直接返回,就不需要添加 else 分支,因为条件为假时代码自然继续执行:

1def sillyFunction(parameter): 2 if parameter is None: 3 return None 4 # 无需添加 'else' 和额外缩进! 5 result = do_something(parameter) 6 return result

省略 else 可以减少嵌套缩进,提升代码可读性。

下面我们编写一个函数,接收一个整数参数,生成对应范围的整数列表:

1def createList(number): 2 return list(range(number)) # 返回列表 3 4def sillyFunction(number): 5 if number is None: 6 return None 7 # 无需添加 'else' 和额外缩进! 8 result = createList(number) 9 return result 10 11print(sillyFunction(None)) # 传入 None 12print(sillyFunction(5)) # 传入 5

使用 yield 而不是 return

一旦从 Python 函数中返回,就无法再次回到该函数中从 return 之后的下一行继续执行。这种行为通常没有问题,但在实现 生成器 时例外。

生成器是一种对象,当被请求时,按需逐个产生一个元素。从调用者的角度看,它像是一个带有内部记忆的函数,记得上次返回结果后停在哪里,并从下一行恢复执行——这看似不可能。

我们可以轻松地自定义生成器。只需将 return 替换为 yield。带有 yield 的函数返回一个 生成器对象。这个对象可以作为参数传递给其他函数,也可以通过内置函数 next()(获取单个元素)或 list()(获取所有元素)来提取生成的值:

1def fortune_teller(attempts=2): 2 for _ in range(attempts): 3 yield bool(random.randint(0, 1)) 4 return 'Do not call me again!' 5 6oracle = fortune_teller(2) # 创建生成器 7print(next(oracle)) 8# 再次调用 9print(next(oracle))

注意:一旦生成器返回(隐式或显式),它就变成空的。

尝试从空的生成器中获取下一个值时,会引发 StopIteration 异常,必要时可以用 try/except 捕获处理:

1next(oracle)

如果向 next() 提供默认值参数,空生成器将返回该默认值,而不抛出异常:

1print(next(oracle, True))

记住,如果对生成器应用 list(),函数会取出所有项,生成器随即变为空。

我们来试一下:

1oracle = fortune_teller(2) 2print(list(oracle))

再次调用 list(oracle) 看看生成器是否为空:

1print(list(oracle))

我们可以使用生成器对象来构造一个可能无限的可迭代对象,并组织一个可能无限的 for 循环:

1def bottomless_mug(): 2 count = 1 3 while True: # 无 return 4 yield f"Coffee #{count}" 5 count += 1 6 7for coffee in bottomless_mug(): 8 print(coffee) 9 if '3' in coffee: break # 若无此行,循环永远不会停止!

生成器函数 在多次调用之间保留其内部状态,可以按需逐个生成长序列(甚至可能是无限序列)。这一点在处理大量数据但内存有限时尤为重要。当我们需要这类特性时,就应该使用生成器!


返回并应用函数

与 C 和 C++ 不同,Python 中的函数是一等对象,这意味着它们可以被其他函数创建、返回,也可以作为参数传递给其他函数。只需将函数名当作变量处理,在需要时使用调用操作符 () 即可。

例如,下面是一个名为 polynomial_factory(coeffs) 的函数,它接收一个多项式系数列表(从最高次幂到最低次幂),并返回一个用于计算该多项式的函数:

1def polynomial_factory(coeffs): 2 def polynomial(x): 3 p = 0 4 for c in coeffs: 5 p = p * x + c 6 return p 7 return polynomial

我们用这个工厂函数构建一个多项式 4x3−7x2+2x+54x^3 - 7x^2 + 2x + 5 的求值器,并在若干点上计算其值:

1poly = polynomial_factory([4, -7, 2, 5]) 2print([poly(x) for x in range(10)])

poly() 函数的行为就像普通函数一样,尽管它是通过工厂函数生成的,而非手动编写。

为了补充多项式工厂,我们可以再创建一个函数,接收一个多项式函数并在指定范围内对其进行评估。换句话说,它将一个函数应用于一个范围。与工厂不同,这个新函数不返回函数,而是将其作为参数接收:

1def apply(func, start, end, step=1): 2 return [func(x) for x in range(start, end, step)] 3 4print(apply(poly, 0, 10))

结果完全一致。这个应用函数的美妙之处在于它的通用性:它可以将任意函数应用于任意范围。


关于 return 和 yield 的小测验

Quiz

单选题

以下哪种情况会使函数返回一个生成器对象?

1 / 5

数据类型与数据结构技巧

函数调用相关技巧