前言
今日在更新程序的时候遇到了个问题。
如何生成多语言的菜单,创建并绑定相应的回调函数?
生成菜单自然是很简单的,一个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 函数生成的函数 f 和 g 都引用了被求和的 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() |
带参装饰器,只需要在原有装饰器函数再进行一次套皮即可。
原文链接:点击打开链接