zoukankan      html  css  js  c++  java
  • 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》

  • 相关阅读:
    (一)MySQL中的常见查询
    PCI设备内存操作函数总结 分类: 浅谈PCI 2014-05-26 17:48 580人阅读 评论(0) 收藏
    HI3531由DMA 发起PCIe 事务 分类: HI3531 浅谈PCI-E windows驱动程序WDM 2014-05-23 11:48 930人阅读 评论(0) 收藏
    hi3531的pcie atu资源重映射 分类: HI3531 浅谈PCI-E 2014-05-21 09:17 695人阅读 评论(0) 收藏
    如何访问pcie整个4k的配置空间 分类: 浅谈PCI-E 2014-05-17 15:13 858人阅读 评论(0) 收藏
    hi3531的pcie控制器使能 分类: HI3531 2014-05-15 18:01 698人阅读 评论(0) 收藏
    hi3531 SDK 编译 kernel, 修改 参数 分类: arm-linux-Ubuntu HI3531 2014-05-07 11:23 1120人阅读 评论(0) 收藏
    如何实现Linux下的U盘(USB Mass Storage)驱动 分类: arm-linux-Ubuntu 2014-05-04 18:03 565人阅读 评论(0) 收藏
    从VGA到GPU!细数二十年显卡发展历程 分类: 生活百科 2014-04-29 17:29 448人阅读 评论(0) 收藏
    VxWorks中的中断应用设计要点 分类: vxWorks 2014-04-29 17:25 494人阅读 评论(0) 收藏
  • 原文地址:https://www.cnblogs.com/lht-record/p/10269638.html
Copyright © 2011-2022 走看看