Python装饰器与闭包

闭包是Python装饰器的基础。要理解闭包,先要了解Python中的变量做用域规则。python

变量做用域规则

首先,在函数中是能访问全局变量的:闭包

>>> a = 'global var'

>>> def foo():
	print(a)

>>> foo()
global var

而后,在一个嵌套函数中,内层函数可以访问在外层函数中定义的局部变量:app

>>> def foo():
	a = 'free var'
	def bar():
	    print(a)
	return bar

>>> foo()()
free var

闭包

上面的嵌套函数就是闭包。闭包是指延伸了做用域的函数,在其中可以访问未在函数定义体中定义的非全局变量。未在函数定义体中定义的非全局变量通常都是在嵌套函数中出现的。框架

上述示例中的变量a就是一个并未在函数bar中定义的非全局变量。对于bar来讲,它有个专业名字,叫作自由变量函数

自由变量的名称能够在字节码对象中查看:spa

>>> bar = foo()
>>> bar.__code__.co_freevars
('a',)

自由变量的值绑定在函数的__closure__属性中:设计

>>> bar.__closure__
(<cell at 0x000001CB2912DF48: str object at 0x000001CB291D3D70>,)

其中保存了对应自由变量的cell对象的序列,cell对象的cell_contents属性保存了变量的值:code

>>> bar.__closure__[0].cell_contents
'free var'

这与JavaScript中闭包的行为是相似的,JavaScript中嵌套函数会将外层函数的活动对象添加到它的做用域链中。但与JavaScript不一样的是,当Python函数中的全局变量或者自由变量是不可变对象(数字、字符串、元组等)时,是只能读取,没法更新的:对象

>>> a = 1
>>> def foo():
	print(a)
	a += 1

>>> foo()
UnboundLocalError: local variable 'a' referenced before assignment

>>> def foo():
	a = 1
	def bar():
	    print(a)
	    a += 1
	return bar

>>> foo()()
UnboundLocalError: local variable 'a' referenced before assignment

两种状况下,都会报错。这并非缺陷,而是Python的设计选择。Python不要求声明变量,可是会假定在函数定义体中赋值的变量是局部变量,以免在不知情的状况下修改全局变量。ip

a += 1a = a + 1相同,编译函数的定义体时,会将a当作局部变量,不会当作自由变量保存。而后尝试获取a的值时,发现a并无绑定值,因而报错。

解决这个问题的办法,一是将变量置于一些可变对象,如列表、字典中:

def foo():
    ns = {}
    ns['a'] = 1
    def bar():
        ns['a'] += 1
        print (ns['a'])
    return bar

另外的方法就是使用global或者nonlocal将变量声明为全局变量或者自由变量:

>>> def foo():
	a = 1
	def bar():
	    nonlocal a
	    a += 1
	    print(a)
	return bar

>>> foo()()
2

当自由变量自己是可变对象时,是能够直接进行操做的:

def make_avg():
    ls = []
    def avg(x):
        ls.append(x)
        print(sum(ls)/len(ls))
    return avg

装饰器

装饰器是可调用对象,参数通常是另外一个函数。装饰器能够以某种方式加强被装饰函数的行为,而后返回被装饰的函数或者将其替换成一个新的函数。

一个最简单的不作任何额外行为的装饰器:

def decorate(func):
    return func

decorate函数就是一个最简单的装饰器,使用方法:

def target():
    pass

target = decorate(target)

Python为装饰器的使用提供了语法糖,能够简便的写为:

@decorate
def target():
    pass

导入时运行

装饰器一个很重要的特性是它是导入时(加载模块时)运行的:

def decorate(func):
    print('running decorator when import')
    return func

@decorate
def foo():
    print('running foo')
    pass

if __name__ == '__main__':
    print('start foo')
    foo()

结果:

running decorator when import
start foo
running foo

能够看到,装饰器是导入时运行的,而被装饰的函数是明确调用时运行的。

装饰器能够返回被装饰的函数自己,和运行时导入的特性结合起来,能够实现简单的注册器功能:

view_registry = []

def register(func):
    view_registry.append(func)
    return func

@register
def view1():
    pass

@register
def view2():
    pass

def main():
    print(view_registry)


if __name__ == '__main__':
    main()

返回新函数

上述装饰器的例子都返回了被装饰的原函数,但装饰器的典型行为仍是返回一个新函数:把被装饰的函数替换成新函数,新函数接受与原函数相同的参数,而且返回原函数本该返回的值。写法相似于:

def deco(func):
    def new_func(*args, **kwargs):
        return func(*args, **kwargs)
    return new_func

这种状况下装饰器就使用到了闭包。JavaScript中的防抖与节流函数就是这种典型的装饰器行为。新函数通常会使用外部装饰器函数中的变量当作自由变量,对函数做出某种加强行为。

举个例子,咱们知道,当Python函数的参数是个可变对象时,会产生意料以外的行为:

def foo(x, y=[]):
    y.append(x)
    print(y)

foo(1)
foo(2)
foo(3)

输出:

[1]
[1, 2]
[1, 2, 3]

这是由于,函数的参数默认值保存在__defaults__属性中,指向了同一个列表:

>>> foo.__defaults__
([1, 2, 3],)

咱们就能够用一个装饰器在函数执行前取出默认值作深复制,而后覆盖函数原先的参数默认值:

import copy

def fresh_defaults(func):
    defaults = func.__defaults__
    def deco(*args, **kwargs):
        func.__defaults__ = copy.deepcopy(defaults)
        return func(*args, **kwargs)
    return deco

@fresh_defaults
def foo(x, y=[]):
    y.append(x)
    print(y)

foo(1)
foo(2)
foo(3)

输出:

[1]
[2]
[3]

接收参数的装饰器

装饰器除了能够接受函数做为参数外,还能够接受其余参数。使用方法是:建立一个装饰器工厂,接受参数,返回一个装饰器,再把它应用到被装饰的函数上,语法以下:

def deco_factory(*args, **kwargs):
    def deco(func):
        print(args)
        return func
    return deco

@deco_factory('factory')
def foo():
    pass

在Web框架中,一般要将URL模式映射到生成响应的view函数,并将view函数注册到某些中央注册处。以前咱们曾经实现过一个简单的注册装饰器,只是注册了view函数,却没有URL映射,是远远不够的。

在Flask中,注册view函数须要一个装饰器:

@app.route('/hello')
def hello():
    return 'Hello, World'

原理就是使用了装饰器工厂,能够简单的模拟一下实现:

class App:
    def __init__(self):
        self.view_functions = {}

    def route(self, rule):
        def deco(view_func):
            self.view_functions[rule] = view_func
            return view_func
        return deco
         
app = App()

@app.route('/')
def index():
    pass

@app.route('/hello')
def hello():
    pass

for rule, view in app.view_functions.items():
    print(rule, ':', view.__name__)

输出:

/ : index
/hello : hello

还可使用装饰器工厂来肯定view函数能够容许哪些HTTP请求方法:

def action(methods):
    def deco(view):
        view.allow_methods = [method.lower() for method in methods]
        return view
    return deco
         
@action(['GET', 'POST'])
def view(request):
    if request.method.lower() in view.allow_methods:
        ...

重叠的装饰器

装饰器也是能够重叠使用的:

@d1
@d2
def foo():
    pass

等同于:

foo = d1(d2(foo))

类装饰器

装饰器的参数也能够是一个类,也就是说,装饰器能够装饰类:

import types

def deco(cls):
    for key, method in cls.__dict__.items():
        if isinstance(method, types.FunctionType):
            print(key, ':', method.__name__)
    return cls

@deco
class Test:
    def __init__(self):
        pass

    def foo(self):
        pass
相关文章
相关标签/搜索