python 装饰器和闭包

讲解闭包前得先讲下变量做用域的问题python

闭包

变量做用域规则

理解闭包里引用的变量,就要理解变量做用域规则,请看下面的例子git

# coding: utf-8
b = 6
def f2(a):
    print(a)
    print(b)

f2(1)

很显然,返回api

1
6

但假如这样呢bash

# coding: utf-8
b = 6
def f2(a):
    print(a)
    print(b)
    b = 9

f2(1)

就会报错闭包

1
Traceback (most recent call last):
  File "E:/code/git/source/master/coreTasker/api/controllers/a.py", line 8, in <module>
    f2(1)
  File "E:/code/git/source/master/coreTasker/api/controllers/a.py", line 5, in f2
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment

首先输出了1,接着print(b)执行不了报错了,一开始觉得会打印6,由于b是个全局变量。app

可事实是,python编译函数的定义体时,它判断b是局部变量,由于在函数中给他赋值了(b=9),那么python就会尝试从本地环境获取b,而本地环境还没给其赋值。函数

若是在函数中赋值时想让解释器把b看成全局变量,那么用global声明:spa

# coding: utf-8
b = 6
def f2(a):
    global b
    print(a)
    print(b)
    b = 9

f2(1)

输出:
1
6

输出正常.net

好,下面切入闭包的概念3d

以实例来讲明,假若有名为avg的函数,它不断返回系列值得平均值,好比下面方式使用

>>> avg(10) # 10/1
10.0
>>> avg(11) # (10+11)/2
10.5
>>> avg(12) # (10+11+12)/3
11.0

avg从何而来,又如何保存历史值呢?

咱们很快可能会想到类-属性的方式, average_oo.py

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

Averager 的实例是可调用对象:

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

或以下函数式实现,average.py

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

以下方式调用

>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

很明显,Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。可是第二个示例中的 avg 函数在哪里寻找 series 呢?

注意,series 是 make_averager 函数的局部变量,由于那个函数的定义体中初始化了series:series = []。但是,调用 avg(10) 时,make_averager 函数已经返回了,而它的本地做用域也一去不复返了。
这时就要引出闭包里的一个关键概念,自由变量(free variable),这是一个技术术语,指未在本地做用域中绑定的变量。在 averager 函数中,series 是自由变量(free variable),见下图

也即闭包的概念就出来了:

一个函数返回了一个内部函数,该内部函数引用了外部函数的相关参数和变量,咱们把该返回的内部函数称为闭包(Closure)(实际上这个引用的变量叫自由变量)

继续,python把局部变量,自由变量放在__code__属性里,见下

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

实际上,series 的绑定在返回的 avg 函数的__closure__ 属性中。avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars 中的一个名称。这些元素是 cell 对象,有个cell_contents 属性,保存着真正的值,以下:

>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

# 可见前面的10,11,12在这里

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义做用域不可用了,可是仍能使用那些绑定。
注意,只有嵌套在其余函数中的函数才可能须要处理不在全局做用域中的外部变量。

注意,在python2中自由变量的认定是有必定条件的,看下面例子

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

这里咱们保存总值以及总个数,按理说ok,但执行时

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

由于咱们在averager内部函数中给count赋值了,这会把count变为局部变量,total也同样

而上面的series=[]没遇到这个问题,是由于咱们没有给series赋值,只是调用append,是基于列表是可变对象的事实

但对数字,字符串,元组等不可变类型来讲,只能读取,不能更新。若是尝试绑定,例如count = count + 1,其实会隐式建立局部变量count,这样count就不是自由变量了,所以不会保存在闭包中。

python3中已经解决了这个问题,引入了nonlocal声明。它的做用是把变量标记为自由变量,即便在函数中为变量赋予新值了,也会变成自由变量。以下:

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

以上闭包的概念基本讲完了,接下来说装饰器

 

装饰器

装饰器本质上就是一个函数,这个函数接收其余函数做为参数,并将其以一个新的修改后的函数进行替换并返回它。后来python提供@语法糖,使程序看起来更简洁。

看以下简单的例子,咱们定义2个普通函数,其中函数d1装饰给f1

# coding: utf-8
def d1():
    print('deco d1')
@d1
def f1():
    print('func f1')

f1()

此时执行会报错

Traceback (most recent call last):
  File "t.py", line 4, in <module>
    @d1
TypeError: d1() takes 0 positional arguments but 1 was given

因此此处对应了装饰器函数的第一点定义:装饰器本质是个函数,须要接收其余被装饰函数做为参数

咱们加上此参数

# coding: utf-8
def d1(func):
    print('deco d1')

@d1
def f1():
    print('func f1')

f2()

执行仍是报错

deco d1
Traceback (most recent call last):
  File "t.py", line 8, in <module>
    f1()
TypeError: 'NoneType' object is not callable

装饰器函数d1执行了,有deco d1输出,但下面的报错了,由于d1装饰器函数并无返回可执行的函数对象,这也就对应了第二点定义:并将其以一个新的修改后的函数进行替换并返回它

此处咱们加上返回

# coding: utf-8
def d1(func):
    print('deco d1')
    return func

@d1
def f1():
    print('func f1')

f1()

执行以下:

deco d1
func f1

可见成功了

因此装饰函数的两点:

1. 以被装饰函数为参数

2. 须要返回可执行函数对象

通常装饰器写法

def decorator(func):
    def wrapper(*args, **kwargs):
        # before do something(like: add_log, cache, check..)
        res = func(*args, **kwargs)
        # end do something
        return res
    return wrapper

但此时返回的是包装后的函数,__name__属性不是原函数名称,要改变这个状态可经过python内置的 functools模块解决

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # before do something(like: add_log, cache, check..)
        res = func(*args, **kwargs)
        # end do something
        return res
    return wrapper

若是装饰器须要处理被装饰函数参数时,用inspect.getcallargs来处理会更友好,他会转变为字典形式

# coding: utf-8
import inspect
from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        func_args = inspect.getcallargs(func, *args, **kwargs)
        print(func_args)
        # before do something(like: add_log, cache, check..)
        res = func(*args, **kwargs)
        # end do something
        return res

    return wrapper

@decorator
def f1(c, e, *args, **kwargs):
    print('func f1')

f1(2, 'a', 1, 2, 3, a='1', b='2')

输出

{'c': 2, 'e': 'a', 'args': (1, 2, 3), 'kwargs': {'a': '1', 'b': '2'}}
func f1

装饰器还一个重要特性:

在加载模块时当即执行,且只执行一次。此特性长被用做函数注册或组件注册

# coding: utf-8
registry = {}

def register(func):
    print('running register(%s)' % func)
    registry[func.__name__]=func
    return func

@register
def f1():
    print('running f1')

@register
def f2():
    print('running f2')

@register
def f3():
    print('running f3')

def main():
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()
    registry.get('f1')()

if __name__=='__main__':
    main()

看成脚本运行时,输出

E:\virtualenv\nowamagic_venv\coreTasker_env1\Scripts\python.exe E:/code/git/source/master/coreTasker/api/controllers/c.py
running register(<function f1 at 0x0000000002712840>)
running register(<function f2 at 0x0000000002990400>)
running register(<function f3 at 0x0000000002990378>)
running main()
registry -> {'f1': <function f1 at 0x0000000002712840>, 'f2': <function f2 at 0x0000000002990400>, 'f3': <function f3 at 0x0000000002990378>}
running f1
running f2
running f3
running f1

可见后面单独执行f1()时并无输出running register ..., 只在加载模块时初始化一次

 

多个装饰器的调用顺序

咱们知道一个装饰器

@d
def f():
    pass

等同于

f = d(f)

多个就是叠加的关系

@d1
@d2
def f():
    pass

等同于

f = d1(d2(f))

 

带参数的装饰器

装饰器如何带参数?很简单,再把装饰器封装一层,返回装饰器

# coding: utf-8
import inspect
from functools import wraps

def decorator_with_param(args1='', args2=''):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            func_args = inspect.getcallargs(func, *args, **kwargs)
            print(func_args)
            # before do something(like: add_log, cache, check..)
            res = func(*args, **kwargs)
            # end do something
            return res

        return wrapper
    return decorator

@decorator_with_param()
def f1(c, e, *args, **kwargs):
    print('func f1')


f1(2, 'a', 1, 2, 3, a='1', b='2')
print(f1.__name__)

实际就是@后面的函数加上()后,就会调用它,若是这个返回是装饰器就能够

简化下就是下面这样

@d(p1, p2)
def f():
    pass

等同于

f = d(p1, p2)(f)

多个的状况

@d1(p1)
@d2(p2)
def f():
    pass

等同于

f = d1(p1)(d2(p2)(f))

见下面

# coding: utf-8
def d1(func):
    def deco(*args, **kwargs):
        print('begin deco d1..........')
        res = func(*args, **kwargs)
        print('end   deco d1..........')
        return res
    return deco

def d2(func):
    def deco(*args, **kwargs):
        print('begin deco d2.......')
        res = func(*args, **kwargs)
        print('end   deco d2.......')
        return res
    return deco

@d1
@d2
def f1(c, e, *args, **kwargs):
    print('func f1')
    return 1


print(f1(2, 'a', 1, 2, 3, a='1', b='2'))
print(f1.__name__)

 返回

begin deco d1..........
begin deco d2.......
func f1
end   deco d2.......
end   deco d1..........
1
deco

注意有点像递归的返回,并且每一个装饰器里func执行结果不返回的话,最后是取不到f1()执行结果的。

 

基于类的装饰器

前面都是基于函数的,其实基于类的也是能够的,利用类的__init__,__call__

class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

@Bold
def hello(name):
    return 'hello %s' % name

>>> hello('world')
'<b>hello world</b>'

能够看到,类 Bold 有两个方法:

  • __init__():它接收一个函数做为参数,也就是被装饰的函数
  • __call__():让类对象可调用,就像函数调用同样,在调用被装饰函数时被调用

还可让类装饰器带参数:

class Tag(object):
    def __init__(self, tag):
        self.tag = tag

    def __call__(self, func):
        def wrapped(*args, **kwargs):
            return "<{tag}>{res}</{tag}>".format(
                res=func(*args, **kwargs), tag=self.tag
            )
        return wrapped

@Tag('b')
def hello(name):
    return 'hello %s' % name

其实就是@Tag('b')会调用__call__返回wrapped

但若是类装饰器要装饰类里的方法呢?

# coding: utf-8
class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

class H(object):
    @Bold
    def hello(self, name):
        return 'hello %s' % name

h = H()
print h.hello('jack')

# 输出
Traceback (most recent call last):
  File "/Users/Teron/Code/Git/Personal/Test/e.py", line 16, in <module>
    print h.hello('jack')
  File "/Users/Teron/Code/Git/Personal/Test/e.py", line 7, in __call__
    return '<b>' + self.func(*args, **kwargs) + '</b>'
TypeError: hello() takes exactly 2 arguments (1 given)

可见报错了,缘由就是被@Bold包装的方法变成了unbound method

print h.hello
print h.hello.func

# 输出
'''
<__main__.Bold object at 0x1023e43d0>
<function hello at 0x1023d4cf8>
'''

这里的<function hello at 0x1023d4cf8>是须要两个参数的,一个self,一个name,咱们只传了一个,因此报错,下面这种方式调用就能够

print h.hello(h, 'jack')

# 输出
'''
<b>hello jack</b>
'''

但咱们不能更改类方法的调用方式呢?只能经过修改类装饰器,经过描述符

# coding: utf-8
class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

    def __get__(self, instance, owner):
        return lambda *args, **kwargs: '<b>' + self.func(instance, *args, **kwargs) + '<b>'


@Bold
def hellof(name):
    return 'hello %s' % name

class H(object):
    @Bold
    def hello(self, name):
        return 'hello %s' % name

print hellof('lucy')

h = H()
print h.hello('jack')

# 输出
'''
<b>hello lucy</b>
<b>hello jack<b>
'''

以上__call__针对函数hellof,__get__针对类H里的hello方法

或者下面这样

# coding: utf-8
import types
class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)  # 手动建立一个绑定方法


@Bold
def hellof(name):
    return 'hello %s' % name

class H(object):
    @Bold
    def hello(self, name):
        return 'hello %s' % name

print hellof('lucy')

h = H()
print h.hello('jack')

# 输出
'''
<b>hello lucy</b>
<b>hello jack</b>
'''

函数装饰器装饰在类方法中是否能够?

def Boldf(func):
    def warp(*args, **kwargs):
        return '<b>' + func(*args, **kwargs) + '</b>'
    return warp

@Boldf
def hellof(name):
    return 'hello %s' % name

class H(object):
    @Boldf
    def hello(self, name):
        return 'hello %s' % name

print hellof('lucy')

h = H()
print h.hello('jack')

# 输出
'''
<b>hello lucy</b>
<b>hello jack</b>
'''

因此最好是用函数装饰器,但函数装饰器有自由变量的问题,这经过nonlocal解决

相关文章
相关标签/搜索