返回相关提示
学习如何更高效地使用 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
我们用这个工厂函数构建一个多项式 的求值器,并在若干点上计算其值:
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
以下哪种情况会使函数返回一个生成器对象?