Python闭包问题的探讨

本文通过实际问题探讨Python闭包,分析闭包的概念、作用及与装饰器的关系。文章详细解释了为何方法一和方法二无法解决多语言菜单问题,而方法三是正确解决方案,揭示了闭包创建独立环境的特性。同时,介绍了如何修改闭包中的“静态变量”,并通过计时器和带参装饰器的例子展示了闭包在装饰器中的应用。
摘要由CSDN通过智能技术生成

前言

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

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

生成菜单自然是很简单的,一个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()

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

 

原文链接:点击打开链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值