Python进阶之[非局部变量,闭包,装饰器]
阅读Tacotron2源码 之 Python进阶
- Non-Local Variable with Nested Function
- Closure in Python
- Decorator
1. Non-Local Variable with Nested Function
在Python中,除了全局变量(Global Variable)和局部变量(Local Variable)之外,还有一种变量叫Non-Local Variable。
Non-Local Variable的存在来源于python允许Nested Function的存在。C/C++中,结构体(类)里面再定义结构体(类)可以做到,但是函数里面再定义函数则不被允许。Python可以实现嵌套函数的原因在于Everything in Python is Object,写嵌套函数的初心无非是外层函数想要利用内层函数的返回值。
无关紧要,python三种变量都是相对的概念,全局变量好理解,就不写了,主要是Local Variable和Non-Local Variable如何区别。直接看一组代码就能明白:
# Version 1
def outer():
y = 40
def inner():
nonlocal y
print("before modified :", y)
y = 50
print("after modified :", y)
inner()
print("outer:", y)
outer()
>>>before modified : 40
>>>after modified : 50
>>>outer: 50
这是一个典型的Nested Funtion(嵌套函数),inner()
函数嵌套在outer()
函数的内部。对于outer()
函数来说,y
就是它的Local Variable;对于inner()
函数来说,有了nonlocal关键字,y
就是它的Non-Local Variable。
此时,你可能会认为Non-Local Variable是用nonlocal关键字定义的,其实不然,没有这个关键字,它也是Non-Local Variable。请看下面一组代码:
# Version 2
def outer():
y = 40
def inner():
# nonlocal y
print("before modified :", y)
# y = 50
# print("after modified :", y)
inner()
print("outer:", y)
outer()
>>>before modified : 40
>>>outer: 40
仔细观察,我只是注释了部分的代码,此时inner()
函数访问(调用)了变量y
,此时对于inner()
函数来说,y
仍然是它的Non-Local Variable。
那么现在一个明显的问题就是,既然Non-Local Variable不依赖于nonlocal关键字来定义,那这个关键字存在的意义是什么?继续看下面的代码:
# Version 3
def outer():
y = 40
def inner():
# nonlocal y
print("before modified :", y)
y = 50
print("after modified :", y)
inner()
print("outer:", y)
outer()
>>>Error
上面Version 3代码只是注释掉了Version 1中的关键字部分,然而运行却是报错的。然后你在看看Version 2和Version 3的区别,就会发现,Version 3试图在没有关键字nonlocal的声明前提下去修改Non-Local Variable,所以它报错了。
总结一下
Non-Local Variable依赖于嵌套函数中存在,并且有两种定义方式——显式定义和隐式定义。显示定义要用nonlocal关键字声明,此时内部函数不仅可以访问还可以修改Non-Local Variable;隐式定义无须声明,但是此时内部函数只有访问而没有修改Non-Local Variable的权利。
2. Python Closure
Closure(闭包),这个概念与上面的Non-Local Variable一样,它依赖于Nested Function而存在。一句话说什么是Closure:
外部函数返回内部函数的嵌套函数,就叫闭包。
观察上面代码,发现外部函数outer()
只是调用了内部函数inner()
,而并没有将其return
出来,所以这个嵌套函数不是闭包。下面是一个闭包的例子:
def print_msg(msg):
def printer():
print(msg)
return printer
another = print_msg("Hello")
another()
>>>"Hello"
因为Everything in Python is Object,所以函数的返回值是另一个函数完全没问题。
闭包的概念就是这么简洁,用法在下面:
del print_msg
another()
>>>Hello
print_msg("Hello")
>>>Traceback (most recent call last):
>>>...
>>>NameError: name 'print_msg' is not defined
也很好懂,我们已经把外部函数print_msg()
杀死了,但是内部函数printer()
却活了下来,因为在杀死print_msg()
之前,another = print_msg("Hello")
,相当于another()
继承了内部函数的地址,所以它仍然存在着。
总结一下:
什么时候需要用到Closure?
- 必须要写嵌套函数的时候
- 嵌套函数的内部函数需要访问非局部变量的时候
- 外部函数需要将内部函数作为返回值
其实我感觉说了跟没说一样。。。目前觉得闭包就是用在下面要说的装饰器中。
3. Decorator
Decorator来源于现实需求,现有的代码功能有短缺,需要添加新功能,但是我想在不大刀改动原有代码的前提下,添加新功能并且新写完的代码具有向后兼容性。
直接用例子说明:
# Version 1
# 阶乘函数
def count(number):
mul = 1
for item in range(1, number):
mul *= item
print(mul)
def cost(func, number):
start = time.time()
func(number)
end = time.time()
cost_time = end - start
return cost_time
time = cost(count, 6)
print(time)
>>>6
>>>1s
上面代码的需求是要计算阶乘函数耗费的时间,一个很直观的想法就是把阶乘函数作为参数传给cost()
函数,这样运行cost()
函数即可。这个做法可以但是并不完美,下面看看装饰器怎么做:
# Version 2
# 这是一个Closure
def cost(func):
def _measure_time(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
elapse = time.time() - start
print("takes {} seconds.".format(elapse))
return _measure_time
@cost
def count(number):
mul = 1
for item in range(number):
mul *= item
print(mul)
count(4)
>>>6
>>>takes 1 seconds.
Version 2就是一个装饰器,它没有改动原始的阶乘函数,只是新写了一个闭包cost
,并且在阶乘函数头上添加了@cost
,使用的时候,发现只运行count()
函数,还输出了消耗的时间,这就叫代码的向后兼容性。
再仔细看一下闭包的写法,用到了两种参数*args
, **kwargs
,这样,无论要装饰或者称为包起来的函数func()
需要什么类型的参数,闭包都可以兼容。
再提一句,Decorator可以叠加,如下:
def star(func):
def inner(*args, **kwargs):
print("*" * 30)
func(*args, **kwargs)
print("*" * 30)
return inner
def percent(func):
def inner(*args, **kwargs):
print("%" * 30)
func(*args, **kwargs)
print("%" * 30)
return inner
@star
@percent
def printer(msg):
print(msg)
printer("Hello")
>>>
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
观察输出,又可以对闭包和装饰器有一个直观的理解。
总结一下:
装饰器就是在代码中很少改动的前提条件下,添加新功能,并且新写完的代码具有向后兼容性。