python函数装饰器

什么是装饰器

装饰器是一个可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会:

1,处理被装饰的函数,然后把它返回

2,将其替换成另一个函数或者对象

 

若有个名为decorate的装饰器,则:

@decorate
def target():
    print('running target()')

等价于:

def target():
    print('running target()')

target = decorate(target)

上述两种写法结果一样,函数执行完之后得到的target不一定是原来那个target()函数,而是decorate(target)返回的函数。

 

确认被装饰的函数会替换成其他函数的一个示例:

def deco(func):
    def inner():
        print('running inner()')
    return inner        #函数deco返回inner对象


@deco            #使用deco装饰target
def target():
    print('running target()')


target()       #调用target,运行inner
print(target)   #target时是inner的引用

如下结果,target被替换掉了,它是inner的引用。

running inner()
<function deco.<locals>.inner at 0x00000253D76B8A60>

严格来说,装饰器只是语法糖。装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。

装饰器两大特性:

1,能把被装饰的函数替换成其他函数(如前所示)

2,加载模块时立即执行

 

python执行装饰器时机(加载模块时)

registry = []         #保存被装饰的函数的引用

def register(func):    #参数是一个函数
    print('running register(%s)' % func)   #显示被装饰的函数
    registry.append(func)
    return func       #返回传入的函数

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

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

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

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

if __name__ == '__main__':
    main()

如上,f1()和f2()被装饰,f3()没有被装饰。结果如下:

running register(<function f1 at 0x0000018E43507A60>)
running register(<function f2 at 0x0000018E43507AE8>)
running main()
registry -> [<function f1 at 0x0000018E43507A60>, <function f2 at 0x0000018E43507AE8>]
running f1()
running f2()
running f3()

如上可知,register在模块中其他函数之前运行(两次),先于main函数执行。调用register时,传给它的参数是被装饰的函数,例如<function f1 at 0x0000018E43507A60>

加载模块后,registry中有两个被装饰函数的引用:f1和f2。这两个函数,以及f3只有在main函数调用时才执行。

若将示例命名为registration.py然后使用import registration.py导入模块,则出现:

running register(<function f1 at 0x0000018E43507A60>)
running register(<function f2 at 0x0000018E43507AE8>)

以上可知,装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才执行。

变量作用域规则

一段代码:

def f1(a):
    print(a)
    print(b)

f1(3)     #报错

代码报错,原因很简单,b没有赋值。现在先给b赋值:

b = 6
def f1(a):
    print(a)
    print(b)

f1(3)

#结果
3
6

b为一个全局变量,正常输出。再加一点料:

b = 6
def f1(a):
    print(a)
    print(b)
    b = 9

f1(3)     #报错

b已经赋值过了,为何上述代码会报错呢。print(a)执行了而print(b)没有执行。事实上,python编译函数定义体时,判断b为局部变量,因为函数中给b赋值了,python从尝试本地环境获取b,调用print(b)时发现b没有绑定值,于是报错。

如果在函数中赋值时想让解释器把b当做全局变量,需要使用global声明

b = 6
def f1(a):
    global b
    print(a)
    print(b)
    b = 9

f1(3)

#结果
3
6

闭包

学习装饰器,必须了解闭包。

闭包:指的是延伸了作用域的函数,其中包含函数定义体中引用,但是不在定义体中定义的非全局变量。关键:它能访问定义体之外的非全局变量

 

定义一个计算平均数的函数,每次新加一个数,得到历史上所有加入的数的平均值。

def make_avg():
    series = []


    def average(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return average


avg = make_avg()
print(avg(10))
print(avg(11))
print(avg(12))

结果:

10.0
10.5
11.0

如上,series是make_avg的局部变量,因为那个函数定义体内初始化了series:serise = [ ]。然而,调用avg(10)时,make_avg函数已经返回了,它本身的作用域也不存在了。

在averager函数中,series是自由变量(在本地作用域中绑定的变量)

 

上图中,averager函数的闭包延伸到那个函数作用域之外,包含series的绑定。

审查编译后的averager:

print(avg.__code__.co_varnames)    #打印局部变量
print(avg.__code__.co_freevars)      #打印自由变量
print(avg.__closure__)                    #__colsure__属性,里面各个元素对应一个自由变量的名称
print(avg.__closure__[0].cell_contents)  #取第一个自由变量的值

('new_value', 'total')
('series',)
(<cell at 0x000001CEB8890B58: list object at 0x000001CEC2214788>,)
[10, 11, 12]

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。

nonlocal声明

每次都要计算所有历史值的总和然后求平均值,显然效率不高,更好的方法是只保留平均值以及个数,然后求平均值。这样写:

def make_avg():
    count = 0
    total = 0


    def average(new_value):
        count += 1
        total += new_value
        return total/count

    return average


avg = make_avg()
print(avg(10))

根据变量域作用规则,count和total不是average函数的局部变量,而直接计算就认为它是局部变量,计算时却又没有绑定值,显然时有问题的。(参见:变量作用域规则)

而上一个average函数也使用了未赋值的series,却没有问题?

事实上,这里利用了列表是可变的对象的这一事实。但是数字,字符串,元组等不可变类型,只能读取,不能更新。若重新绑定,会隐式创建同名局部变量

python3引入的nonlocal声明解决了这个问题。上述代码改为:

def make_avg():
    count = 0
    total = 0


    def average(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total/count

    return average


avg = make_avg()
print(avg(10))

一个简单装饰器

输出函数运行时间的装饰器:

import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)   #获取原函数结果
        elapsed = time.perf_counter() - t0  #运行时间
        name = func.__name__       #函数名
        arg_str = ', '.join(repr(arg) for arg in args)  #函数参数
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked   #返回内部函数,取代被装饰的函数

使用该装饰器:

@clock
def snooze(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

if __name__ == '__main__':
    print('*' * 40, 'calling snooze(1)')
    snooze(1)
    print('*' * 40, 'calling factorial(6)')
    print('6!=', factorial(6))

结果:

**************************************** calling snooze(1)
[1.00001869s] snooze(1) -> None
**************************************** calling factorial(6)
[0.00000073s] factorial(1) -> 1
[0.00001210s] factorial(2) -> 2
[0.00002016s] factorial(3) -> 6
[0.00002896s] factorial(4) -> 24
[0.00003666s] factorial(5) -> 120
[0.00004582s] factorial(6) -> 720
6!= 720

 在这个示例中,factorial保存的是clocked函数的引用,每次调用factorial(n),执行的都是clocked(n):

1)记录初始时间

2)调用原来的factorial函数,保存结果

3)计算时间

4)格式化并打印收集的数据

5)返回第2)步保存的结果

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同参数,而且返回被装饰的函数本身该返回的值,同时做一些额外操作

标准库中的几个装饰器

1.functools.wraps         

//保留原函数的属性,保证装饰器不会对被装饰函数造成影响

def deco(func):
    @functools.wraps(func)
    def inner():
        print('running inner()')
    return inner        #函数deco返回inner对象


@deco            #使用deco装饰target
def target():
    print('running target()')


print(target) 

不加这个装饰器时:

<function deco.<locals>.inner at 0x00000253D76B8A60>

使用@functools.wraps装饰器之后 ->显示的是原本的函数,保留了原函数__name__,__doc__等属性

<function target at 0x000001B79BD34A60>

2.functools.lru_cache 

//缓存数据,避免传入相同的参数时的重复计算

使用递归算法生成第n个斐波那契数:

@clock                     #使用clock装饰器
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

if __name__ == '__main__':
    print(fibonacci(6))

结果:

[0.00000037s] fibonacci(0) -> 0
[0.00000073s] fibonacci(1) -> 1
[0.00004692s] fibonacci(2) -> 1
[0.00000000s] fibonacci(1) -> 1
[0.00000037s] fibonacci(0) -> 0
[0.00000037s] fibonacci(1) -> 1
[0.00001540s] fibonacci(2) -> 1
[0.00003042s] fibonacci(3) -> 2
[0.00009237s] fibonacci(4) -> 3
[0.00000037s] fibonacci(1) -> 1
[0.00000000s] fibonacci(0) -> 0
[0.00000037s] fibonacci(1) -> 1
[0.00001356s] fibonacci(2) -> 1
[0.00002749s] fibonacci(3) -> 2
[0.00000037s] fibonacci(0) -> 0
[0.00000037s] fibonacci(1) -> 1
[0.00001356s] fibonacci(2) -> 1
[0.00000037s] fibonacci(1) -> 1
[0.00000037s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00001430s] fibonacci(2) -> 1
[0.00002749s] fibonacci(3) -> 2
[0.00005388s] fibonacci(4) -> 3
[0.00009421s] fibonacci(5) -> 5
[0.00020087s] fibonacci(6) -> 8
8

许多重复的计算导致浪费时间,使用lru_cache改善:

@functools.lru_cache()              #lru_cache是参数化装饰器,必须加上()    可看下节  参数化装饰器
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

时间从0.0002s减少到0.00008s

[0.00000037s] fibonacci(0) -> 0
[0.00000037s] fibonacci(1) -> 1
[0.00005242s] fibonacci(2) -> 1
[0.00000073s] fibonacci(3) -> 2
[0.00006635s] fibonacci(4) -> 3
[0.00000073s] fibonacci(5) -> 5
[0.00008138s] fibonacci(6) -> 8
8

lru_cache使用两个可选参数来配置:lru_cache(maxsize=128,typed=False)

maxsize:缓存个数,满了之后会被扔掉(least recently used  扔掉最近最少使用的数据),理论上应设置为2的幂次

typed:设置为True时,不同类型的参数的运算结果会分开保存,例如1和1.0

3.functools.singledispatch     

//类似于c++重载,使用singledispatch装饰的普通函数会变为泛函数:根据第一个参数类型以不同方式执行相同操作的一组函数(称之为单分派;而根据多个参数选择专门的函数,称为多分派) 

python不支持重载方法或函数,使用if/elif/elif来处理不同类型的数据显得稍显笨拙,不便于扩展。而functools.singledispatch提供了类似于重载的方式,根据传入的不同类型返回结果

from functools import singledispatch

@singledispatch
def show(obj):
    print (obj, type(obj), "obj")

@show.register(str)
def _(text):
    print (text, type(text), "str")

@show.register(int)
def _(n):
    print (n, type(n), "int")
show(1)
show("helloworld")
show([1])

结果:

1 <class 'int'> int
helloworld <class 'str'> str
[1] <class 'list'> obj

叠放装饰器

@d1
@d2
def f():
    xxx

等同于:

def f():
    xxx

f = d1(d2(f))

参数化装饰器

python把被装饰的函数作为第一个参数传给装饰器函数。如果要让装饰器接受其他函数,就需要创建一个装饰器工厂函数把参数传给它返回一个装饰器,然后再把它应用到要装饰的函数上。

对于clock装饰器,加一点料,让用户传入一个格式字符串,控制被装饰函数的输出:

import time
from functools import wraps

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({arg_str}) -> {_result}'

def clock(fmt=DEFAULT_FMT):
    def decorate(func):
        @wraps(func)
        def clocked(*args, **kwargs):
            t0 = time.perf_counter()
            result = func(*args)   #获取原函数结果
            elapsed = time.perf_counter() - t0  #运行时间
            name = func.__name__       #函数名
            arg_list = []
            if args:
                arg_list.append(', '.join(repr(arg) for arg in args))
            if kwargs:
                pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
                arg_list.append(', '.join(pairs))
            arg_str = ', '.join(arg_list)
            _result = repr(result)
            print(fmt.format(**locals()))
            return result
        return clocked
    return decorate

if __name__ == '__main__':

    @clock()
    def snooze(seconds):
        time.sleep(seconds)

    for i in range(3):
        snooze(.123)

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

这个clock装饰器,clock是参数化装饰器工厂函数decorate真正的装饰器,clocked包装被装饰的函数;clocked会取代被装饰的函数,返回被装饰的函数原本返回值,decorate返回clocked,clock返回decorete

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

默认输出格式:'[{elapsed:0.8f}s] {name}({arg_str}) -> {_result}'    上述代码输出:

[0.12360011s] snooze(0.123) -> None
[0.12296046s] snooze(0.123) -> None
[0.12395127s] snooze(0.123) -> None

调整格式:

if __name__ == '__main__':

    @clock('{name}({arg_str}) dt = {elapsed:0.8f}s')
    def snooze(seconds):
        time.sleep(seconds)

    for i in range(3):
        snooze(.123)

输出结果

snooze(0.123) dt = 0.12316317s
snooze(0.123) dt = 0.12387173s
snooze(0.123) dt = 0.12382994s

由于类也是可调用对象,而调用类即调用类的__call__方法,因此类装饰器需要实现__call__方法。事实上,装饰器最好通过实现了__call__方法的类来实现而不是通过普通函数来实现。

以上来自《流畅的python》

posted @ 2019-01-15 21:44  Sakura_lht  阅读(854)  评论(0编辑  收藏  举报