python 装饰器基础

Python 装饰器

本文内容参考 fluent python

装饰器基础知识

装饰器一般将一个函数替换为另一个函数,举例来说:

@deco
def foo():
    print("running foo")

def foo():
    print("running foo")
foo = deco(foo)

这两种写法的作用是一致的。所以目前可以直观的理解为,装饰器就是在某个函数的外面又包了一层。

什么时间执行装饰器

Python装饰器的一个关键特性是:它们在被装饰的函数定义后立即执行。

也就是说函数装饰器在导入模块时立刻运行,而被装饰的函数只在明确调用时运行。

tmp = []
def reg(func):
    print("aaaa")
    tmp.append(func)
    return func

@reg
def f1():
    print("running f1")

当你在命令行中输入以上代码时,会得到aaaa的输出,即使你还没有明确的调用f1,你会发现其实reg已经执行了,此时查看tmp也会看到f1的信息。而你明确调用f1时,即输入f1(),会得到running f1的输出。

当然上面只是一个说明装饰器何时执行的demo,在实际使用中,装饰器和被装饰函数往往不在同一个模块中,而且装饰器返回的函数往往是内部定义的,并将其返回。

实际使用

这里书中举了一个前文的例子,个人觉得并不需要了解,只需要知道,装饰器可以在这个函数被调用前做一些其他的事情就OK了。

更常见的使用方式是,装饰器内部定义一个新的内部函数,并将其返回,替代被装饰的函数。说到这里,就不得不先提一下什么是闭包。

闭包

先来看一段代码:

b = 6
def foo(a=3):
    print(a)
    print(b)
    b = 9

foo()

如果按照C或者C++中的经验,我们会认为输出是3和6,但实际上,是下面的结果:

3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in foo
UnboundLocalError: local variable 'b' referenced before assignment

这是Python的一个设计选择(这不是bug,这是语言特性):Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。

也就是说,如果我们把b=9这行去掉,那么还是会输出3和6。那么可以通过global关键字让解释器把b当做全局变量。了解完Python的作用域,接下来就开始说明闭包。

首先说明。闭包与匿名函数不是一个概念,闭包指延伸了作用域的函数,包含在函数定义体中引用、但是不在定义体中定义的非全局变量,关键在于能否访问定义体之外定义的非全局变量。

举例来说,我们想计算一个不断增长的序列的均值:

class Averager:
    def __init__(self) -> None:
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

这个实现并不能算错,而是不够pythonic,下面是函数式实现:

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

avg = mkae_averager()

我们在调用avg时,因为make_averager已经返回,所以它的本地作用域被回收,但是实际上series是一个自由变量,并未在本地作用域中绑定。那么averager的闭包延伸到了他自己的作用域之外。

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

python3提供了nonlocal关键字,作用是把变量标记为自由变量。具体用处如下:

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

上面是make_averager的另一种实现,存在问题,这里因为averager内部的count进行了赋值操作,所以解释器默认为局部变量,因此闭包中并未绑定count为自由变量,total同理,此时就可以用nonlocal:

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

一个简单的装饰器

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

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kw):
        t0 = time.perf_counter()
        res = func(*args, **kw)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kw:
            pairs = ['%s=%r'%((k, w) for k, w in kw.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, res))
        return res
    return clocked

标准库中的装饰器

lru_cache

实现了备忘功能,有效减少递归深度。

singledispatch

根据第一个参数的类型,以不同的方式执行相同的操作的一组函数。

参数化装饰器

因为装饰器本质上是函数,因此,也可以定义一些参数。但这个时候往往需要在装饰器里再多一层嵌套。

def reg(active=True):
    def deco(func):
        if active:
            func("deco")
        else:
            func("undeco")
        return func
    return deco

deco就是多的那一层嵌套。

posted @ 2021-07-25 17:37  xinze  阅读(49)  评论(0编辑  收藏  举报