Python闭包问题的探讨

版权声明:本文为博主原创文章,转载请注明出处。

前言

今日在更新程序的时候遇到了个问题。

如何生成多语言的菜单,创建并绑定相应的回调函数?

生成菜单自然是很简单的,一个for循环就好了,但是生成相应函数就???

正文

在解决以下事件的过程中,又思考得出了别的内容。

续:前言事件

情况如下,现有语言列表及两个业务方法。

1
2
3
4
5
6
7
8
9
10
11
# 现有两种语言,不排除以后会扩展
#(即便只有两种语言也不写hardcode,便于日后扩展)
languages = ['en', 'cn']

def set_language(lang):
# TODO: 这里是设置语言的业务代码
pass

def add_menu(name, callback):
# TODO: 这里是添加菜单的业务代码
pass
  • 最初的想法如下(方法一):
1
2
3
for lang in languages:
# 菜单回调函数包含一个参数,该参数用于获取触发的菜单。(但是没有用到这个参数,所以添加 _ 占位)
add_menu(lang, callback=lambda _: set_language(lang))

但是菜单无论如何点击,都会设置成 ‘cn’。(失败)

这是因为,创建回调函数(lambda表达式)时,以引用的方式将 lang 变量传入了函数;而 lang 变量是随着for循环改变的,而for循环最后一个‘cn’。(见languages list)

  • 略加改进之后(方法二):
1
2
for lang in languages:
add_menu(lang, callback=lambda _: (lambda x: set_language(x))(lang))

本以为,将操作再作为函数封装起来,然后再将 lang 变量传入参数 x(参数变量是临时的)即可解决问题(规避引用)。但是,结果与前面相同。(失败)

  • 正解操作:
1
2
for lang in languages:
add_menu(lang, callback=(lambda x: lambda _: set_language(x))(lang))

来看看这个方法与前面两种方法的区别:

简单来说,方法一与方法二是一致的,想法都是创建一个带占位符(菜单参数)的回调函数,然后在回调函数里设置语言(for循环中的 lang 变量)。

而正解方法三的想法,则是创建一个生成回调函数的函数,然后将 lang 变量注入生成函数,最终返回设置各个语言的回调函数。(感觉有点反过来了)

由于定义了生成函数的函数,所以此处可对其进行复用。(减少创建次数)

1
2
3
g_set_lang = lambda x: lambda _: set_language(x)
for lang in languages:
add_menu(lang, callback=g_set_lang(lang))

至此,问题解决。

什么是闭包

在前面的事件中,思考方法二的代码要如何修改时,请教了友人 Musoucrow,其提到了闭包这一知识点。(似乎前面根据参数生成函数这种操作就是闭包???)

经过一番查阅资料发现:闭包是由函数及其相关引用环境组合而成的实体;闭包就是指有权访问另一个函数作用域中的变量的函数。(???)

来看下面一个例子:

这是一个简单的求和函数。

1
2
3
4
5
6
7
def sum(l: list):
s = 0
for i in l:
s += i
return s

print(sum([1, 2, 3, 4, 5])) # >>> 15

假如有以下要求:求和函数非立即运算,而是在后面再调用进行运算。

那么就需要将函数拆分成两部分:一是将被求和的变量初始化(生成环境),二是计算的过程。

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
def sum(l: list):
def core():
s = 0
for i in l:
s += i
return s
return core

f = sum([1, 2, 3, 4, 5])
g = sum([1, 2, 3])

print(f()) # >>> 15
print(g()) # >>> 6

此时,调用 sum 函数生成的函数 fg 都引用了被求和的 list - 变量 l,且变量 l 在两个函数间相互独立。

也就是在生成函数时,同时创建了一套变量的环境。(可以理解成方法中的静态变量)

据我所理解,闭包就是一个可以根据参数生成的,独立环境(一个或多个静态变量)与函数的集合体。

修改“静态变量”

这里是一个加法函数的生成函数,逻辑上看并没有什么问题。

1
2
3
4
5
6
7
8
9
def plus(init):
s = init
def core(x):
s += x
return s
return core

f = plus(10)
print(f(1))

当运行时会提示:

1
2
3
4
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 4, in core
UnboundLocalError: local variable 's' referenced before assignment

这时,如果要修改“静态变量”(外函数的变量),需要使用 nonlocal 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def plus(init):
s = init
def core(x):
nonlocal s
s += x
return s
return core

f = plus(10)
print(f(1)) # >>> 11
print(f(2)) # >>> 13
print(f(3)) # >>> 16

g = plus(0)
print(g(1)) # >>> 1
print(g(2)) # >>> 3
print(g(3)) # >>> 6

print(f(0), g(0)) # >>> 16 6

由此可见,函数 f 与函数 g 之间,各拥有着一套独立的环境。

闭包与装饰器

在查阅资料的过程中,见到一种说法:闭包用于实现装饰器。

猛然想起,python里面的语法糖,装饰器!

计时器

以下是一个业务函数,为了检测其运行时间,通常会这么做:

1
2
3
4
5
6
7
8
9
from time import time

def xxx():
# TODO: 这里是业务代码
pass

t = time()
xxx()
print('time usage: %f' % (time() - t))

装饰器,可用于对已有的函数进行包装,将某函数(如xxx)传入装饰器函数内,生成新的函数,并覆盖原函数。

具体看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from time import time

def time_count(func):
def core(*args, **kwargs):
t = time()
result = func(*args, **kwargs)
print('%s time usage: %f' % (func.__name__, time() - t))
return result
return core

@time_count
def xxx():
# TODO: 这里是业务代码
pass

xxx()

常见的装饰器有如 @property@staticmethod 等,其本质应该还是闭包(创建函数及其独立环境)。

带参装饰器

函数是可以具备参数的,所以装饰器也是同样道理可以带参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from time import time

def time_count(to_int=False):
def time_count_core(func):
def core(*args, **kwargs):
t = time()
result = func(*args, **kwargs)
if to_int:
print('%s time usage: %d' % (func.__name__, int(time() - t)))
else:
print('%s time usage: %f' % (func.__name__, time() - t))
return result
return core
return time_count_core

# 此处须加上括号调用带参装饰器。
@time_count()
def xxx():
# TODO: 这里是业务代码
pass

@time_count(True)
def yyy():
# TODO: 这里是业务代码
pass

xxx()
yyy()

带参装饰器,只需要在原有装饰器函数再进行一次套皮即可。

# python
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×