在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。————摘自 维基百科
一、局部/全局变量
定义在方法内的变量是局部变量,不能在方法外做引用;定义在模块最外层的变量是全局变量,它是全局范围内可见的。对于作用域的概念,相信大家都理解,这里就不做赘述了。
那么问题来了,当方法内部有嵌套方法时,嵌套方法能否使用父方法中定义的局部变量呢?答案是可以,上栗子:
def foo():
a = 10
def sub():
return a
return sub()
此时调用foo()的返回值就是10哦,所以说嵌套方法可以用父方法中声明的局部变量。
二、闭包
上面的概念似乎还算比较容易理解,那么什么是闭包呢?我们来看下面一段代码:
def foo():
a = 10
def sub():
return a
return sub
乍一看这段代码似乎和上面的没有区别,但是注意,最后foo()返回的是sub方法本身而不是sub()哦。我们来尝试着调用一下foo():
>>> foo()
<function sub at 0x1019ab398>
调用foo()返回的是sub()方法,因为foo()返回的就是sub方法本身而不是sub方法的返回值,这里也不难理解,那么我们该如何调用sub()方法呢:
>>> test = foo() #1
>>> test()
10
这时,sub()就被成功调用了。看到这里一切都好理解对不对?怀疑看了一篇假闭包文对不对?但实际上上面的代码确实构建了一个闭包。下面仔细讲解一下:
a作为foo()方法的局部变量,仅仅在foo()方法执行时才能够使用,一但foo()方法执行过后,我们就认为foo()方法中的内部变量已从内存中释放了。
再回到上述的代码,在#1处我们调用了foo()方法对不对?foo()方法执行完了对不对?这时候foo()方法的局部变量a要从内存中释放了对不对?然鹅。。。并没有,当我们调用test()的时候,局部变量a依旧有效。
这就是闭包的作用,使得局部变量在方法外部能够被访问。上面的test就是一个闭包,它由sub()方法以及a变量组成,这些变量的值也能够始终保存在内存中,不以foo()的终结而释放。
三、闭包作用
那么为什么要使用闭包呢?闭包避免了使用全局变量,此外,它又能够将函数和其操作的某些数据关联起来。见上栗a并不是全局变量,但通过闭包又能让sub()方法和局部变量a关联起来。
或许上面的描述有些抽象,我们上栗子吧,先说一下需求,要求将一段文字头尾的空格去掉。超级简单对不对,我们用my_strip()方法来实现:
def my_strip(text):
value = text.strip()
return value
这时候,又有一个需求,将文字外部包裹html标签<p>,你可以这么做:
def my_strip(test):
value = text.strip()
return "<p>{}<p>".format(value)
这么做固然可以,但对于变换莫测的需求变更,这么做显然不够合理,如果能将标签元素名作为一个变量传进来岂不是更好?继续改造:
>>>def add_tag(tag):
... def my_strip(text):
... value = text.strip()
... return "<{tag}>{value}<{tag}>".format(tag=tag, value=value)
... return my_strip
>>> adder = add_tag("p")
>>> adder(" cool ")
<p>cool<p>
这样标签名称就可以通过一个参数传入啦,但是这离优雅还差一点点,想要更加优雅就需要引入装饰器。装饰器也是基于闭包的一种应用场景,下面我们来简要介绍一下装饰器,先看下面这段代码:
>>>def decorator(func):
... name = 'p'
... def add_tag(text):
... value = func(text)
... return "<{name}>{value}<{name}>".format(name=name, value=value)
return add_tag
>>>def my_strip(text):
... value = text.strip()
... return value
>>>my_strip=decorator(my_strip)
>>>my_strip(" cool ")
这段代码将my_strip()方法单独拆出来,作为参数传入decorator()方法。
而在调用时将my_strip
作为闭包名称,现在任何对my_strip
的调用都不会得到原始的my_strip
,我们也可以这么理解my_strip
成了一个“被装饰过”的版本,有没有领悟到“被装饰”的思想呢?
在python中,通过在函数定义前加@符号可以实现如上对函数的“装饰”,在上面的用了一个“被装饰过”的函数来替换原来的闭包来实现了“装饰”,上面的代码也等价于:
@decorator
def my_strip(text):
value = text.strip()
return value
又离优雅近了一步。但是注意到,这里的标签名是直接定义在decorator中的,可不可以作为参数传入呢?继续往下看:
def tag(name):
def decorator(func):
def wrapper(text):
value = func(text)
return "<{name}>{value}<{name}>".format(name=name, value=value)
return wrapper
return decorator
@tag("p")
def my_strip(text):
value = text.strip()
return value
我们将decorator()方法外面又包了一层tag方法,专门用于传标签名,这里也实现了多层闭包,此时将装饰器中传入标签名就可以加上想要的标签。而且这也做到了加标签
与字符串处理
的业务分离,不管是加<p>
加<a>
,不管是将字符串首位空格去掉、将字符全转大些,这段代码都能轻松做到。而且它还可以支持加多重标签哦,看下面:
@tag("div")
@tag("p")
def my_strip(text):
value = text.upper()
return value
print my_strip("cool") #打印出<div><p>COOL<p><div>
大写的优雅~
补充:斐波那契数列的优化装饰器写法
from functools import wraps
def memo(func):
cache={}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args]=func(*args)
return cache[args]
return wrap
@memo
def fib(i):
if i<2: return 1
return fib(i-1)+fib(i-2)
print(fib(10))
再补充:时隔一年半,最近发现类作为装饰器使用,会很优雅
class DBFixture(object):
'''
这是一个数据库连接的装饰器代码,具体实现功能就是初始调用某个DAO方法时连接数据库,调用完之后关闭数据库连接
'''
def __init__(self, database='test_database'):
self.database = database
def __db_conn(self):
engine = create_engine('mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(CONFIG['DB_LOCAL']['USERNAME'],
CONFIG['DB_LOCAL']['PASSWORD'],
CONFIG['DB_LOCAL']['HOST'],
CONFIG['DB_LOCAL']['PORT'],
self.database), echo=False)
Session = sessionmaker()
Session.configure(bind=engine)
self.session = Session(autocommit=True)
return self.session
def __db_finalizer(self):
self.db_session.flush()
self.db_session.close()
def __call__(self, func):
def wrapper(*args, **kwargs):
session = self.__db_conn()
res = func(session=session, *args, **kwargs)
self.__db_finalizer()
return res
return wrapper
@DBFixture('database_name')
def get_tg_team_by_id(session, id):
team = session.query(TgTeam).filter(TgTeam.id == id).one()
return team