这是一篇译文,原文地址:https://realpython.com/inner-functions-what-are-they-good-for/
1. 封装
内部函数可以免受函数之外的情况的影响,也就是说,对于全局命名空间而言,它们是隐藏的。
下面是一个简单的例子:
def outer(num1):def inner_increment(num1): # 对外部空间隐藏return num1 + 1num2 = inner_increment(num1)print(num1, num2)inner_increment(10)
# outer(10)
如果我们直接调用 inner_increment()
函数,会有报错信息:
Traceback (most recent call last):File "inner.py", line 7, in <module>inner_increment()
NameError: name 'inner_increment' is not defined
注释掉对 inner_increment()
的直接调用,对外部的函数传入参数 10,即 outer(10)
是可以运行的:
10 11
注意:这只是一个例子,虽然这些代码可以运作,但就这个函数而言,可能更好的方式是把inner_increment()
定义为存在于外部空间的“私有”函数,即在函数名前加一个下划线前缀,即_inner_increment()
。
下面这个嵌套函数可能是一个更好的使用内部函数的例子:
def factorial(number):# 处理错误if not isinstance(number, int):raise TypeError("Sorry. 'number' must be an integer.")if not number >= 0:raise ValueError("Sorry. 'number' must be zero or positive.")def inner_factorial(number):if number <= 1:return 1return number*inner_factorial(number-1)return inner_factorial(number) # 调用外部函数
print(factorial(4))
在这里,我们把参数验证放在外部函数,而在内部函数中处理关键步骤。
2. 避免自我重复(DRY原则)
有时,我们可能会在一个大型函数中,重复地使用一些代码。比方说,我们写一个处理文件的函数,同时支持文件名或文件对象作为参数:
def process(file_name):def do_stuff(file_process):for line in file_process:print(line)if isinstance(file_name, str):with open(file_name, 'r') as f:do_stuff(f)else:do_stuff(file_name)
注意:再次提醒,可能更常见的情况是,我们直接把 do_stuff()
放在外部,作为一个私有函数,但显然,必要时我们也可以把它作为内部函数隐藏起来。
我们可以写一个更具体的例子。
假如说,我们想了解纽约市的 WIFI 热点数据,可以直接在网上下载对应的 CSV 文件,然后进行统计:
def process(file_name):def do_stuff(file_process):wifi_locations = {}for line in file_process:values = line.split(',') # 创建一个字典,记录统计数据 wifi_locations[values[1]] = wifi_locations.get(values[1], 0) + 1max_key = 0for name, key in wifi_locations.items():all_locations = sum(wifi_locations.values())if key > max_key:max_key = keybusiness = nameprint(f'纽约市总共有 {all_locations} 个 WIFI 热点,'f'{business} 提供的热点最多,有 {max_key} 个。')if isinstance(file_name, str):with open(file_name, 'r') as f:do_stuff(f)else:do_stuff(file_name)
运行后得到结果如下:
>>> process('NAME_OF_THE.csv')
纽约市总共有 1251 个 WiFi 热点,Starbucks 提供的热点最多,有 212 个。
3. 闭包与工厂函数
接下来我们要讨论的是使用内部函数最重要的理由。在之前的例子中,内部函数都是一个常规函数,只是恰好被嵌套在另一个函数中而已。也就是说,我们完全用其它方式定义它们(如之前已经提示的),并非一定要使用内部函数。
而在考虑闭包的时候,我们就必须使用嵌套函数了。
什么是闭包
闭包可以使内部函数记住它所在空间的具体状态。新手们常常以为内部函数就是闭包,准确地说,应该是内部函数制造了闭包。所谓闭包,所“封闭”的是函数帧中的局部变量。
一个例子
以下是一个例子:
def generate_power(number):""" Examples of use:>>> raise_two = generate_power(2)>>> raise_three = generate_power(3)>>> print(raise_two(7))128>>> print(raise_three(5))243"""# 定义内部函数def nth_power(power):return number ** power# 将函数作为外部函数的结果返回return nth_power
对例子的解释
让我们看看这个例子中具体发生了什么:
generate_power()
是一个工厂函数,每次调用它时,会返回一个新创建的函数,因此,raise_two
、raise_three
指向的是这些新创建的函数;- 这些新创建的函数,需要一个参数
power
,返回的值是number**power
; - 那么,这个
number
的值是怎么来的呢?这就是闭包发生作用的地方:nth_power()
函数是从外部函数,即工厂函数获取number
的值的。整个过程可以分解步骤如下:
- 调用外部函数:
generate_power(2)
; - 创建函数
nth_power()
,它需要一个参数power
; - 保存
nth_power()
函数帧的状态,其中包括number=2
; - 将保存的函数帧状态传递给
generate_power()
函数; - 返回
nth_power()
函数;
换句话说,闭包为 nth_power()
函数提供了初始化数据并将它返回。因此,我们调用这个被返回的函数时,总是可以在其函数帧中找到 number=2
。
一个实际应用
现在,让我们考虑一个真实世界中的例子:
def has_permission(page):def inner(username):if username == 'Admin':return "'{0}' does have access to {1}.".format(username, page)else:return "'{0}' does NOT have access to {1}.".format(username, page)return innercurrent_user = has_permission('Admin Area')
print(current_user('Admin'))random_user = has_permission('Admin Area')
print(random_user('Not Admin'))
这是一个简化版的权限判断函数,我们也可以做简单修改,从 session 中获取用户信息,进而判断这个用户是否具有接入某个路由的权限。显然,我们会从数据库中查询用户权限,而不是检查用户名是否等于 'Admin'
。
总结
闭包与函数工厂是内部函数最常见、最主要的用处。大多数情况下,如果你看到一个带装饰器的函数,这个装饰器就是一个函数工厂,它以一个函数作为参数,并返回一个新的函数,新的函数使用闭包包括了作为参数的函数。
换句话说,装饰器就是一个语法糖,它的基本流程其实和上面所举的 generate_power()
的例子是一致的。
以下是最后一个例子:
def generate_power(exponent):def decorator(f):def inner(*args):result = f(*args)return exponent**resultreturn innerreturn decorator@generate_power(2)
def raise_two(n):return n print(raise_two(7))@generate_power(3)
def raise_three(n):return n print(raise_two(5))
如果你的代码编辑器允许的话,可以尝试把 generate_power(exponent)
和 generate_power(number)
并排对比,以理解我们所讨论的概念。(比如说,可以用 Sublime Text 中的分栏功能)
如果你还没写出这两个函数的话,建议还是亲自在编辑器中敲出来一次,对编程新手来说,写代码就行骑自行车:你必须亲自上手。
敲出这些代码后,你就能看出,它们产生了类似的结果,但也有一些不同。对于还没有用过装饰器的人来说,注意到这些不同,就是理解它们的开始。
END
公众号:ReadingPython