python-函数的嵌套、变量作用域、闭包

函数的嵌套

函数的嵌套有两个方面的作用:

第一,函数的嵌套能够保证内部函数的隐私。内部函数只能被外部函数所调用和访问,不会暴露在全局作用域,因此,如果你的函数内部有一些隐私数据(比如数据库的用户、密码等),不想暴露在外,那你就可以使用函数的的嵌套,将其封装在内部函数中,只通过外部函数来访问。比如:

1
2
3
4
5
6
def connect_DB():
def get_DB_configuration():
...
return host, username, password
conn = connector.connect(get_DB_configuration())
return conn

第二,合理使用函数嵌套,能够提高程序的运行效率,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def factorial(input):
# validation check
if not isinstance(input, int):
raise Exception('input must be an integer.')
if input < 0:
raise Exception('input must be greater or equal to 0' )
...

def inner_factorial(input):
if input <= 1:
return 1
return input * inner_factorial(input-1)
return inner_factorial(input)


print(factorial(5))

这里,我们使用递归的方式计算一个数的阶乘。因为在计算之前,需要检查输入是否合法,所以我写成了函数嵌套的形式,这样一来,输入是否合法就只用检查一次。而如果我们不使用函数嵌套,那么每调用一次递归便会检查一次,这是没有必要的,也会降低程序的运行效率。

函数变量作用域

全局变量是定义在整个文件层次上的,比如:

1
2
3
4
5
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
if value < MIN_VALUE or value > MAX_VALUE:
raise Exception('validation check fails')

这里的 MIN_VALUEMAX_VALUE 就是全局变量,可以在文件内的任何地方被访问,当然在函数内部也是可以的。不过,我们不能在函数内部随意改变全局变量的值。比如,下面的写法就是错误的:

1
2
3
4
5
6
7
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
...
MIN_VALUE += 1
...
validation_check(5)

如果运行这段代码,程序会报错:

1
UnboundLocalError: local variable 'MIN_VALUE' referenced before assignment

这是因为,Python 的解释器会默认函数内部的变量为局部变量,但是又发现局部变量MIN_VALUE并没有声明,因此就无法执行相关操作,所以,如果我们一定要在函数内部改变全局变量的值,就必须加上global这个声明:

1
2
3
4
5
6
7
8
MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
global MIN_VALUE
...
MIN_VALUE += 1
...
validation_check(5)

这里的 global 关键字,并不表示重新创建了一个全局变量 MIN_VALUE,而是告诉 Python 解释器,函数内部的变量 MIN_VALUE,就是之前定义的全局变量,并不是新的全局变量,也不是局部变量。这样,程序就可以在函数内部访问全局变量,并修改它的值了。另外,如果遇到函数内部局部变量和全局变量同名的情况,那么在函数内部,局部变量会覆盖全局变量。

另外,对于嵌套函数来说,内部函数可以访问外部函数定义的变量,但是无法修改,若要修改,必须加上nonlocal这个关键字:

1
2
3
4
5
6
7
8
9
10
11
12
def outer():
x = "local"
def inner():
nonlocal x # nonlocal 关键字表示这里的 x 就是外部函数 outer 定义的变量 x
x = 'nonlocal'
print("inner:", x)
inner()
print("outer:", x)
outer()
# 输出
inner: nonlocal
outer: nonlocal

如果不加上 nonlocal 这个关键字,而内部函数的变量又和外部函数变量同名,那么同样的,内部函数变量会覆盖外部函数的变量。

闭包

闭包其实和嵌套函数类似,不同的是,这里的外部函数返回的是一个函数,而不是一个具体的值,返回的函数通常赋予一个变量,这个变量可以在后面被继续调用。

比如,我们想计算一个数的 n 次幂,用闭包可以写成下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of # 返回值是 exponent_of 函数

square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
square
# 输出
<function __main__.nth_power.<locals>.exponent(base)>

cube
# 输出
<function __main__.nth_power.<locals>.exponent(base)>

print(square(2)) # 计算 2 的平方
print(cube(2)) # 计算 2 的立方
# 输出
4 # 2^2
8 # 2^3

这里外部函数 nth_power() 返回值,是函数 exponent_of(),而不是一个具体的数值。需要注意的是,在执行完square = nth_power(2)和cube = nth_power(3)后,外部函数 nth_power() 的参数 exponent,仍然会被内部函数 exponent_of() 记住。这样,之后我们调用 square(2) 或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义了。

为什么要闭包呢?上面的程序,也可以写成下面的形式:

1
2
def nth_power_rewrite(base, exponent):
return base ** exponent

这样是可以的,使用闭包的一个原因是让程序变得更简洁易读,当需要计算很多个数的平方时:

1
2
3
4
5
6
7
8
9
10
11
12
# 不使用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...

# 使用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...

使用闭包,每次调用都可以少传入一个参数,表达更为简洁,而且,当函数开头需要做一些额外的操作,而我们又需要多次调用这个函数时,将那些额外的操作放在外部函数,就可以减少多次调用导致的不必要的开销,提高程序的运行效率,另外,闭包常常和装饰器一起使用。