Python3 闭包、装饰器以及作用域LEGB法则

2021-02-21 补充一点理解

闭包本身也不是python特有的东西,很多语言中都有。函数嵌套,外部函数包含内部函数,从而内部函数成为闭包(这个“闭”字,体现在环境的私有化,所谓环境可以简单理解成闭包自带一个独立的变量作用域)

很多人学python都是先学的类,类属性,类方法,类实例这些概念,后接触的闭包。

我个人认为这么理解可能对新人更好理解一些:
你可以把闭包当成是特殊的类看待。
闭包和类都是需要实例化后才能使用的
类实例后,一般都是包含属性和方法的,
而闭包实例本身就是唯一的方法,nonlocal声明过的变量就是实例属性(编写内部函数(闭包)时,其外部函数的作用,就是把它的变量,通过nonlocal作用域扩展到内部函数(闭包)。在实例化时,由于闭包会携带它的环境,所以由父函数扩展来的变量作为环境的一部分,成为了实例属性)

=======================
闭包也就是装饰器的实现,其作用在于:
1、不改变原函数内代码段;
2、不改变原函数名称;
3、为原函数增加新的功能,改变输出结果等等。

本文涉及到以下3个知识点:
1、作用域LEGB法则(知道的可以跳过)
2、闭包
3、装饰器

1、作用域LEGB法则

寻找变量的调用顺序,就是采用的LEGB原则(就近原则):L>E>G>B

  • 局部L(local)
  • 闭包E(enclosing) 闭包(函数嵌套)L的父
  • 全局G(global)
  • 内置B(built-in)

用伪代码描述一下,就是:

B      # Python内置模块的命名空间
------ # 以下才是你写的代码正文
G
def f1():
    E
    def f2():
        L

也就是从下往上、从里往外逐级查找。

  • B是指内置,不是你自己定义的,不用管

  • L和G,简而言之就是函数里和函数外

  • E,如果函数外还有函数嵌套,则里面的是L,外面的是E,最外面是全局G

  • L可以通过global声明,变为与全局共用的变量,也就是L、G共用变量

  • L可以通过nonlocal声明,变为与其父级共用的变量,也就是L、E共用变量

  • 这里的共用是指的读取和修改
    如果只是需要读取,可以不用声明,因为只要不存在同名变量,程序就会默认根据就近原则,逐级向上查找读取对应名称的变量;但如果你用到了修改,则程序会报错

举个例子

def fa():
	a = 0
	def fb():
		print(a)
	fb()
	
fa()

fa()嵌套fb(),子函数中没有对变量a进行赋值操作,也是a=xxx之类的。所以在fb()的L中没有的时候,可以向上层也就是父级寻找,使用E作用域的中的变量。

假设这里改了一句

def fa():
	a = 0
	def fb():
		print(a)
		a = 1 # 这里加一句对a的赋值
	fb()
	
fa()

# 输出结果
Traceback (most recent call last):
  File "test.py", line 5, in fb
    print(a)
UnboundLocalError: local variable 'a' referenced before assignment
[Finished in 0.2s with exit code 1]

你会发现在print(a)的时候,就出现了a未定义的错误。原因在于fb()的L局部作用域中,有对a进行了赋值这样的操作,a不能使用父级的定义。而在打印输出时,局部函数中a尚未定义,导致程序报错。

那么局部就不可以修改父级了么?其实是可以的。
原理和global声明变量,局部变全局是一个道理。

def fa():
	a = 0
	def fb():
		nonlocal a
		print(a)
		a = 1
	fb()

fa()

使用nonlocal声明后,a的作用域就扩展到了父级,就又可以使用E嵌套作用域了。

2、闭包说明

首先以一段闭包实现的代码为例:

def fa():
	a = 0
	def fb():
		nonlocal a
		print(a)
		a = 1
	return fb

f = fa()
f()
f()

# 输出结果
0
1

将闭包函数,之前的嵌套函数对比
发现代码基本一致,只有最后的fb()变成了return fb

def fa():				# def fa():
	a = 0				# 	a = 0
	def fb():			# 	def fb():
		nonlocal a		#		nonlocal a
		print(a)		#		print(a)
		a = 1			#		a = 1
	return fb			#	fb()

这就导致程序没有执行内部函数fb,而是将内部函数打包作为输出。(闭包执行流程可以看当前页最下方的测试记录)

所以通过f = fa()返回的函数对象f,其实执行的是,打包好的内部函数fb()的内容,就是下面这段

	def fb():
		nonlocal a
		print(a)
		a = 1

但它的特别之处在于:
f创建时所携带的父层变量a会一直存在于内存中,不会因为函数执行完成而消失(除非你主动del f删掉)

f = fa()
f()
f()

# 输出结果
0
1

其结果分别为0和1的原因在于:
第1次f(),读取了f初始化时的a值进行输出为0,然后通过a=1a才变成1
第2次f(),因为a已经变成1,所以a值输出为1,然后通过a=1a依旧是1

其创建特征在于:函数嵌套,返回函数对象,返回那个函数引用了父层变量。
其使用特征在于:保存局部信息不被销毁。

3、装饰器说明

说完了闭包,来说装饰器。

首先写一个闭包程序,
函数funca将输入参数+1,作为结果输出。
闭包实现的是任意输入函数,将其输出结果求平方。
所以将funca打包成new_funca后,输出结果变成了 ( 1 + 1 ) 2 = 4 (1+1)^2=4 (1+1)2=4

def build_bibao(f):
	def bibao(*args):
		result = f(*args)
		return result**2
	return bibao

def funca(num):
	return num+1

new_funca = build_bibao(funca)
print(new_funca(1))

# 输出结果
4

那么将上面代码用@简写,其实就是装饰器的实现。

def build_bibao(f):
	def bibao(*args):
		result = f(*args)
		return result**2
	return bibao

@build_bibao
def funca(num):
	return num+1

print(funca(1))

# 输出结果
4

装饰器实际上对输入函数funca做了包装,然后给你返回了包装好的,附加了新功能(结果求平方)的funca

============================================================

下面是自己研究闭包时候,做的一些测试(只做记录,可以忽略)

代码如下:

def funx(x):
	return x*x
	
def build_bibao(func):
	def bibao(j):
		return func(j)
	return bibao
	
new_funx = build_bibao(funx)
new_funx(5)

添加打印输出,确认其执行过程:

def funx(x):
	return x*x
	
def build_bibao(func):
	print(0,type(func),func)
	def bibao(j):
		print(3,type(j),j)
		print(4,type(func.__name__),func.__name__)
		print(5,type(func(j)),func(j))
		return func(j)
	print(1,type(bibao),bibao)
	return bibao
	
new_funx = build_bibao(funx)
print(2,type(new_funx),new_funx)
new_funx(5)

可以依照打印序号,确认执行顺序和结果

0 <class 'function'> <function funx at 0x000002AA4EA72EA0>
1 <class 'function'> <function build_bibao.<locals>.bibao at 0x000002AA4EDD8730>
2 <class 'function'> <function build_bibao.<locals>.bibao at 0x000002AA4EDD8730>
3 <class 'int'> 5
4 <class 'str'> funx
5 <class 'int'> 25
[Finished in 0.2s]
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
闭包装饰器是一种特殊的装饰器,它使用闭包的概念来实现。闭包是指一个函数可以访问并操作其外部函数中定义的变量。在Python中,闭包装饰器可以用于给函数添加额外的功能,同时保持函数的原始定义不变。 引用中的示例展示了装饰器传参的形式。在这个例子中,outer函数是一个装饰器,它将inner函数作为子函数返回,并在inner函数中添加了额外的功能。通过使用@outer装饰器语法,我们可以在add函数上应用outer装饰器,从而在调用add函数时执行装饰器中的代码。 引用中的示例展示了多层装饰器的使用。在这个例子中,outer1和outer2函数分别是两个装饰器,他们都返回一个inner函数。通过使用@outer1和@outer2装饰器语法,我们可以在outers函数上应用这两个装饰器,并在调用outers函数时按照装饰器的定义顺序执行相关的代码。 引用提供了关于Python闭包装饰器的使用方法的总结。这篇文章通过示例代码详细介绍了闭包装饰器的使用,对于学习和工作有一定的参考价值。 引用中的示例展示了装饰器的形式。在这个例子中,outer函数是一个装饰器,它将inner函数作为子函数返回,并在inner函数中添加了额外的功能。通过使用@outer装饰器语法,我们可以在add函数上应用outer装饰器,从而在调用add函数时执行装饰器中的代码。 综上所述,Python闭包装饰器是一种利用闭包概念实现的特殊装饰器,可以用于给函数添加额外的功能。这种装饰器可以通过装饰器传参的形式、多层装饰器的形式或普通的装饰器形式来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值