原文发表在我的博客主页,转载请注明出处
前言
如果把python当作脚本语言,每次就是写个几十行上百行来处理数据的话,装饰器也许不是很必要,但是如果要开发一个大型系统,装饰器是躲不开的,最开始体会ryu的装饰器之美是在阅读ryu源码的时候,用python官网的一句话来说,learning about descriptors creates a deeper understanding of how python works and an appreciation for the elegance of its design。本篇文章从闭包讲起,也从闭包结束,因为如果理解了闭包,也就理解了装饰器。
python函数作用域LEGB
不论在什么语言中,变量都是必不可少的,在python中,一般存在以下几个变量类型,local:函数内部作用域;enclosing:函数内部与内嵌函数之间;global:全局作用域;build-in:内置作用域。解释器在遇到变量的时候,会按照如下的顺序进行查找:L > E > G > B,简称LEGB。下面以一个简单的函数来说明上述过程:
#coding:utf-8
#global var
passline = 60
def func(val):
# for func, it's local var
# for in_func, it's enclosing var
passline = 90
if val >= passline:
print "pass"
else:
print "fail"
def in_func():
print val
return in_func
def Max(val1, val2):
# max is a built-in fun
return max(val1, val2)
f = func(89)
f()
print Max(90, 100)
local,global,build-in这三个类型比较容易理解,enclosing变量呢?下一节详细讲解。
闭包理解与使用
首先理解下python中的函数,在python中,函数是一个对象(可以通过type函数查看),在内存中占用空间;函数执行完成之后内部的变量会被解释器回收,但是如果某变量被返回,则不会回收,因为引用计数器的值不为0;既然函数也是一个对象,他也拥有自己的属性;对于python函数来说,返回的不一定是变量,也可以是函数。
上一节提到了enclosing变量,在func函数中又定义了一个函数in_func,它输出了变量val,在输出过程中查找变量的时候发现本地没有,所以他就会去func函数里面找并且找到了,这个变量就是enclosing变量。在上面一段代码中,敏感的人可能已经发现了问题,分析代码执行的过程,func函数返回了in_func函数给了f,但是没有返回变量,所以在调用func完成之后val变量应该已经被解释器回收,但是在执行了f函数之后却仍然输出了val的值89,为什么呢?其原因就是:如果引用了enclosing作用域变量的话,会将变量添加到函数属性中,当再次查找变量时,不是去代码中查找,而是去函数属性中查找。可以通过如下代码进行验证:
#coding:utf-8
passline = 60
def func(val):
print "%x" %id(val)
if val >= passline:
print "pass"
else:
print "fail"
def in_func():
print val
in_func()
return in_func
f = func(89)
f() #in_func
print f.__closure__
执行上面的代码,可以发现f函数的__closure__属性拥有一个变量,这个变量的ID和func函数中val变量的ID一样。
上面的代码是用来判断学生的成绩是否及格,即在百分制中60分及格,如果现在需要添加新的功能,即150分制中90分作为及格线,如何完成代码呢?最简单的就是我们创建两个函数,分别为func_100和func_150来完成判断,判断逻辑完全一样,代码如下:
#coding:utf-8
def func_150(val):
passline = 90
if val >= passline:
print "pass"
else:
print "fail"
def func_100(val):
passline = 60
if val >= passline:
print "pass"
else:
print "fail"
func_100(89)
func_150(89)
如果再增加应用场景呢?这样重复而没有任何技术含量的代码的增添十分繁琐,我们可以使用新的方法——闭包来解决这个问题。什么是闭包呢?闭包就是内部函数中对enclosing作用域的变量进行引用。代码如下:
#coding:utf-8
def set_passline(passline):
def cmp(val):
if val >= passline:
print "pass"
else:
print "fail"
return cmp
f_100 = set_passline(60)
f_150 = set_passline(90)
print type(f_100)
print f_100.__closure__
f_100(89)
f_150(89)
在上述代码中,我们定义了一个set_passline函数,这个函数返回cmp函数,在这个函数中定义了cmp函数,在cmp函数中是我们之前的逻辑。在之后不论要进行多少分制的及格判断,只需要调用set_passline函数设置及格线就好,我们以百分制60分及格为例,分析上述代码的执行过程,基本分为两步,一是set_passline函数把返回值cmp函数给f_100,同时将60作为属性给cmp函数,二是f_100函数的执行其实相当于执行存储了passline的cmp函数。
接下来考虑一个问题,上面的passline是整数,能否换成函数?既然变量和函数都是对象,而且以python的灵活性,答案是肯定的。下面的代码同时描述了闭包的另一个应用场景,同时展示了如何使用。
#coding:utf-8
def my_sum(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return sum(arg)
def my_average(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return sum(arg)/len(arg)
def dec(func):
def in_dec(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return func(*arg)
return in_dec
# 1.dec return in_dec -> my_sum
# 2.my_sum = in_dec(*arg)
my_sum = dec(my_sum)
在上面的代码中,首先定义了两个函数,这两个函数的功能大同小异,分别为求和函数和求平均值函数,由于求平均值元祖的长度不能为空,同时元祖中的数据都应该为整数,所以在每个函数中都先需要参数检查,本着以人为本的方针,这部门代码应该复用。所以在下面定义了另外一个函数dec,这个函数的参数为一个函数,返回值为一个内嵌函数,这个内嵌函数的逻辑和上面求和求平均值的逻辑一样,先判断,再返回结果。这个函数如何使用呢?比如现在我们要求和,代码如下:
def my_sum(*arg):
return sum(arg)
def dec(func):
def in_dec(*arg):
if len(arg) == 0:
return 0
for val in arg:
if not isinstance(val, int):
return 0
return func(*arg)
return in_dec
# 1.dec return in_dec -> my_sum
# 2.my_sum = in_dec(*arg)
my_sum = dec(my_sum)
my_sum = dec(my_sum)函数分两步,首先my_sum得到dec返回的in_dec函数,其次调用in_dec函数。
所以,遵上来看,闭包可以在很大程度上实现封装和代码复用。
装饰器
最开始就说,这篇博客始于闭包,终于闭包,所以装饰器不多说,只说四句话:
1.装饰器就是对闭包的使用;
2.装饰器用来装饰函数;
3.返回一个函数对象,被装饰的函数接收;
4.被装饰函数标识符指向返回的函数对象。
总结
接触python装饰器很久了,殊不知从闭包更容易理解装饰器。