简单粗暴的角度看待Python闭包

两段代码案例(运行环境Python3)

案例1 使用CallCountPrint装饰器打印Func1Func2的调用次数

class Count(object):
	def __init__(self):
		self.mCount = 0

def CallCountPrint(func):
	c = Count()
	def Wrapper():
		c.mCount += 1
		func()
		print ("{}第{}次调用".format(func.__name__, c.mCount))
	return Wrapper

@CallCountPrint
def Func1():
	pass

@CallCountPrint
def Func2():
	pass

Func1()				# 打印结果: Func1第1次调用
Func1()				# 打印结果: Func1第2次调用
Func1()				# 打印结果: Func1第3次调用
Func2()				# 打印结果: Func2第1次调用
Func2()				# 打印结果: Func2第2次调用
Func2()				# 打印结果: Func2第3次调用

案例2 GetList返回三个函数,各自调用后返回值对比

def GetList():
	l = []
	i = 0
	for i in range(3):
		obj = object()
		def Print():
			return i, obj
		l.append(Print)
	i = 4
	obj = None
	return l

funcList = GetList()
i0, obj0 = funcList[0]()
i1, obj1 = funcList[1]()
i2, obj2 = funcList[2]()
print i0					# 打印结果: 4
print i1					# 打印结果: 4
print i2					# 打印结果: 4
print obj0					# 打印结果: None
print obj1					# 打印结果: None
print obj2					# 打印结果: None
print obj0 is obj1				# 打印结果: True
print obj0 is obj2				# 打印结果: True
print obj1 is obj2				# 打印结果: True

上面两段代码如果理顺了,估计闭包这个坎也迈过去了。


关于嵌套函数和闭包

本人曾在一个写Python的项目组工作,项目规范文档里写着"禁止使用闭包"六个大字,确实,这玩意Hold不住或稍有不慎就容易挖下天坑,如内存泄漏等,但运用得当确实是能写出精妙高级的设计。

嵌套函数和闭包之所以会成为很多人的噩梦,是因为学Python初期各个部分都是十分简单易懂的,但突然来了个断崖式突兀的概念,缓不过来,难以融入自己的理解体系,导致似懂非懂得学一遍忘一遍。其实有一个角度可以帮助我们很丝滑得去理解嵌套函数和闭包。

Python 一切皆对象

面向对象的角度,def定义的代码片段就是一个函数对象函数对象可被调用执行,也拥有属性,这个函数对象也叫闭包,这个角度而言就是这么简单,事实上也确实这么简单。


面向对象的角度看待两个案例

如何看待 def

首先不要觉得def这玩意有什么很夸张的语法功能。

a=1
b=[]
c={} 

完全是跟上述这类赋值语法一视同仁的,变量名=值

# 第1段
def Func():
	print ("hello")

# 第2段
Func = None

运行第1段后,相当于:

Func = 一个函数对象

运行第2段后,相当于:

Func = None

这是相同的过程,只是给变量名为Func的变量赋值,赋的什么值?值就是一个函数对象,也即是Func变量引用了一个函数对象,调用Func的时候,Func只是指了一条路,找到那个函数对象并调用。

# Func变量引用了一个函数对象,函数对象的内容是打印hello
def Func():
	print ("hello")

# Func1变量也引用了与Func1相同的函数对象
Func1 = Func

# Func变量引用了None
Func = None

此时运行Func()是报错的,Func此时是None,运行Func1自然是会打印出hello,因为此时 Func1变量引用了Func原先引用的函数对象。

Func = 1
Func1 = Func 
Func = 3

说白了过程就与上例一样简单,不要想着Func加在def后就有什么特殊的,这仅仅是产生一个函数对象并被一个变量引用的语法上固定要求的写法,嵌套函数只是写法上是函数里定义函数,实际上定义函数的过程其实也就只是一个很简单的创建对象并赋值的过程。

案例1

CallCountPrint装饰了Func1函数和Func2函数。

def CallCountPrint(func):
	c = Count()
	def Wrapper():
		c.mCount += 1
		func()
		print ("{}第{}次调用".format(func.__name__, c.mCount))
	return Wrapper

这段代码很一目了然,执行CallCountPrint时变量Wrapper引用了一个新的函数对象并返回,最后 Func1Func2两个变量分别引用了返回的函数对象。注意,Func1Func2引用的是两个不同的函数对象,只是内容是一样的,即是定义Wrapper时那一段。

接下来最硬核的一个点:

调用Func1时,实际上是执行Wrapper定义的内容,也就是

c.mCount += 1
func()
print "{}第{}次调用".format(func.__name__, c.mCount)

讲道理,当Func1被装饰后,它就跟CallCountPrint没任何关系了,CallCountPrint生命周期也结束了,里面定义的参数func,局部变量c等也无了,那Func1在执行时,这些变量c,变量func是从哪里拿的呢?

答案就是从函数对象自身的属性里拿。

要记住一个硬核知识点,在定义这个内嵌函数的时候,这个内嵌函数运行时调用到的那些定义在内嵌函数外的变量,都会被扔到函数对象一个的属性里。至于这个属性叫啥,长啥样,先不用理,可能是个元组,可能是个列表,可能是个字典,反正里面存着函数对象被调用时需要的东西,至于这些变量是以什么形式存在里,存的是变量本身,还是变量引用的对象?也先不用理,反正有关系

可直接通过打印Func1的属性,也就是CallCountPrint返回的函数对象的属性。

print(dir(Func1))

# 打印结果
# ['__annotations__', '__call__', '__class__', 
# '__closure__', '__code__', '__defaults__', '__delattr__', 
# '__dict__', '__dir__', '__doc__', '__eq__', ......

有个属性叫__closure__,翻译就叫闭包,打印一下__closure__

print(type(Func1.__closure__))

# 打印结果: <class 'tuple'>

print(Func1.__closure__)

# 打印结果(<cell at 0x0000027DCC5C01F8: Count object at 0x0000027DCC9A6B08>, 
#<cell at 0x0000027DCC769078: function object at 0x0000027DCC84DF78>)

__closure__ 属性就是一个元组,里面存放着两个cell对象元素,从cell元素描述来看,显而易见,分别与Func1运行时的cfunc有关系。

案例1里打印的结果就理顺了,每Func1调用一次,Func都会从自己的__closure__属性里获取到 Func1被装饰时的变量c,和参数func,然后变量c引用的Count类对象的属性mCount+1,并执行参数func引用的函数对象(Func1起初被def赋值的函数对象)。Func2同理。

案例1里的闭包是什么?

内嵌函数Wrappercfunc这些内嵌函数外的局部变量一起构成了闭包,其实这个函数对象整体就是闭包。

案例2

案例2里容易让初学者难以接受的一点是,这def怎么还又嵌在for里面了?还存到了一个列表里?

案例1同理,只是把函数对象加到一个列表里而已。

... ...

for i in range(3):
    Print = 一个函数对象
    l.append(Print)

... ...

另外一个匪夷所思的现象:

在循环里,生成一个函数对象Print时,由于Print定义时调用了iobj,因此,iobj会被保存并且已经保存在了函数对象Print__closure__里,此刻循环里的情况是:

i=0,obj=一个新的object()

i=1,obj=另一个新的object()

i=2,obj=另另一个新的object()

按平时Python,Java等语言惯性,讲道理在__closure__里的是当下的iobj引用的对象,即 __closure__里的情况是:

__closure__i=0__closure__obj=一个新的object()

__closure__i=1__closure__obj=另一个新的object()

__closure__i=2__closure__obj=另另一个新的object()

# 预想的赋值过程
obj = None
Print.__closure__.obj = obj
obj = object()

预想中过程应该是与上例效果一致的,一个新的Print函数对象 的__closure__引用了当时obj引用的对象后,obj变量无论引用什么都和上一次循环的Print函数对象的__closure__里保存的obj没有关系了。

print(i0)					# 打印结果: 4
print(i1)					# 打印结果: 4
print(i2)					# 打印结果: 4
print(obj0)					# 打印结果: None
print(obj1)					# 打印结果: None
print(obj2)					# 打印结果: None
print(obj0 is obj1)			# 打印结果: True
print(obj0 is obj2)			# 打印结果: True
print(obj1 is obj2)			# 打印结果: True

案例2打印结果却与预想大相径庭,说明Print__closure__里的cell对象并不是直接引用obj引用的对象那么简单,反而像是对变量自身的引用,即Print关注的不是obj变量引用的对象,而是obj变量自身,运行时变量obj引用的是哪个对象,Print调用的就是哪个对象。

preview

 

学过C++的可能会有感觉,这像是指向指针的指针。

可以简单实验一下

def Func():
    num = 0
    def PrintNum():
        print(num)
    PrintNum()      # 打印结果 0
    num = 1
    PrintNum()      # 打印结果 1
    num = 2
    PrintNum()      # 打印结果 2
    del num
    PrintNum()      # 报错 free variable 'num' referenced before assignment in enclosing scope

Func()

从实验结果来看,这个逻辑是成立的,当删除num变量时,调用PrintNum会报错,因为关注的是变量本身,变量被删除了,PrintNum就相当于尝试访问一个被删掉的东西。

全局变量就更好理解,因为全局变量本身就不需进入闭包的__closure__,运行时直接访问得到。

num = 0

def Func():
    def PrintNum():
        print(num)
    return PrintNum

func = Func()
func()      # 打印结果 0
num = 1
func()      # 打印结果 1
num = 2
func()      # 打印结果 2
del num
func()      # 报错 name 'num' is not defined

num这个全局变量在此模块被del后,也就直接报错,全局变量与闭包是没什么关系的。

根据这个规矩,案例2的运行过程和打印结果也是直接理顺了。


需要硬性理解的自由变量

上面两个案例里,还有一点不讲道理的情况需要硬性得去认识。

  • 案例2里 i变量 虽然没像实验时在GetList函数里被del掉,但GetList函数调用结束后,i变量作为GetList的局部变量,本应随着GetList 函数调用结束而消失,但随后 Print 调用 i 却没报错。

这是固定的规则,在Python层面是理不顺的,知道就行。

i这些在函数里局部变量,只要不显式得del掉,即使函数调用结束,它就能安全得活下来并被内嵌函数调用。从术语来说,i也称为自由变量


超多重嵌套函数

做个测试,为了区分打印时认清各个变量,各个自由变量分别赋值不同类型的对象:

# 多重嵌套函数测试

def Func1():
	dictVar = {}
	listVar = []
	def Func2():
		setVar = set()
		def Func3():
			tupleVar = ()
			listVar.append(None)
			def Func4():
				print(dictVar)
				print(setVar)
				print(tupleVar)
			return Func4
		return Func3
	return Func2

func2 = Func1()
func3 = func2()
func4 = func3()

print func2.__closure__
# 打印结果
# (<cell at 0x0000000003504228: dict object at 0x0000000002FB2378>, 
#  <cell at 0x000000000349F888: list object at 0x000000000EF35548>)

print func3.__closure__
# 打印结果
# (<cell at 0x0000000003504228: dict object at 0x0000000002FB2378>, 
# <cell at 0x000000000349F888: list object at 0x000000000EF35548>, 
# <cell at 0x0000000003532468: set object at 0x0000000003440C88>)

print func4.__closure__
# 打印结果
# (<cell at 0x0000000003504228: dict object at 0x0000000002FB2378>, 
# <cell at 0x0000000003532468: set object at 0x0000000003440C88>, 
# <cell at 0x0000000003532498: tuple object at 00000000002F31048>)

func2的打印:

dictVarlistVar都进入了它的__closure__ ,但Func2并没直接调用到这两个,真正调用到他的是它的子嵌套函数Func3和子子嵌套函数Func4

func3的打印:

dictVarlistVarsetVar都进入了它的__closure__,但Func3并没直接调用到listVar,真正调用到它的是子嵌套函数Func4

可以大胆得理解,子嵌套函数里的__closure__里的元素来源于上一层函数的局部变量上一层函数的__closure__里。

# Func3片段
      ...	
                def Func3():
			tupleVar = ()
			listVar.append(None)
			def Func4():
				print (dictVar)
				print (setVar)
				print (tupleVar)
			return Func4
	...

上面是执行func3()时的环境,这里和Func1Func2里定义了啥已没关系了,Func3运行过程里生成Func4函数对象时,Func4会将变量dictVarsetVartupleVar扔到它自己的__closure__里,tupleVarFunc3的局部变量,但setVardictVar哪里来?

结合Func3__closure__打印,显而易见,Func4__closure__里的setVardictVar只能来源于Func3__closure__


Py2和Py3闭包部分差异

  • 上述有个例子是执行del删除了一个自由变量,后续闭包调用时报错,这是Py3环境下的情况,在Py2里直接在语法上就限制了不能del自由变量。
  • Py2无法对自由变量重新赋值,Py3可以用nonlocal
# 运行环境Py2
def Func():
	num = 0
	def Inner():
		num = 1
	Inner()
	print num
        # 打印结果: 0
	print Inner.__closure__
        # 打印结果: None
Func()

Inner里尝试重新对num赋值1,结果只是相当于在Inner里生成了一个同名i局部变量,跟外面的 i 没有任何关系,甚至 i 还丧失了进入Inner__closure__的机会。

# 运行环境Py3
def Func():
	num = 0
	def Inner():
		nonlocal num
		num = 1
	Inner()
	print(num)
        # 打印结果: 1

Func()

Py3引入了nonlocal,使得可以在内嵌函数里修改自由变量的引用。


End

相信这些案例如果都理顺了,再去看书本上的解释应该是得心应手了,估计闭包这个坎也算是迈过了,也足够去应付针对Python闭包的开发了。

上述仅仅是基于Python层面上的推敲解释,很多地方描是不准确的,只是方便去辅助理解使用,至于它更准确具体的底层原理,那就得自行深入到c层去研究解释了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值