乐趣区

关于python:Python-装饰器

Python 中所有皆对象,函数也是对象。函数能够赋值给一个变量,函数能够当作参数传递个另一个函数,函数能够通过 return 语句返回函数。而装璜器就是一个可能接管函数并返回函数的函数。这话乍听起来有点绕,但装璜器实质上就是一个函数。
既然要学习装璜器,首先就要晓得它用于什么场景,装璜器通过面向切面编程来加强代码的健壮性,比方:记录日志,解决缓存,权限校验等。接下来咱们就一步一步的学习 Python 中装璜器的用法。

先来看一个简略的函数定义,函数只有一个性能,打印 Hello World:

def hello():
    print('Hello World!')

当初新的需要来了,要在原有的函数执行前退出日志记录性能,于是就有了上面这段代码:

def hello():
    print('run hello')
    print('Hello World!')

当初下面的问题解决了,只须要减少一行代码就能搞定。但问题是,理论工作场景下,咱们可能须要批改的并不只是一个 hello 函数,有可能是 10 个、20 个函数同时须要减少日志性能。这个时候问题就来了,咱们不太可能挨个函数顺次复制这一行代码,况且那个时候有可能减少的不只是一行代码,可能上百行。并且这样就会造成呈现大量的反复代码,当代码呈现过多反复,你就要小心了,它很容易引起意想不到的 bug,并且难以排查及保护。
一个很容易想到的办法是定义一个专门打印日志的函数 log,而后在每个函数中都调用一下 log 函数:

def log():
    print('run hello')

def hello():
    log()
    print('Hello World!')

这样做还是须要修 hello 函数外部的代码,不是说不能这样做,但这样做显然违反了 开闭准则 思维 —— 对已实现的性能代码关闭,对扩大凋谢。尽管这句话通常用在面向对象编程思维中,但函数式编程同样实用。
咱们能够思考用高阶函数的形式来解决这个问题,还是定义一个 log 函数,但这次它接管一个函数作为参数,这个函数外部先执行打印日志的性能,在 log 函数最初调用传递进来的函数:

def log(func):
    print('run hello')
    func()

def hello():
    print('Hello World!')

log(hello)

下面的代码就利用了函数能够当作参数传给另一个函数的个性,解决了须要批改原来函数外部代码的问题。这样做尽管性能上实现了,并且没有毁坏原有函数外部的逻辑,然而却毁坏了函数调用方的代码逻辑。也就是说,在原来代码中所有调用 hello 函数的语句不得不从 hello() 改为 log(hello),这样做仿佛更麻烦了些。

简略装璜器

那么,当初就是该引出 装璜器 这个概念的时候了, 装璜器 十分善于用 Pythonic 的形式解决这类问题。
来看一个最简略的装璜器的写法:

def log(func):
    def wrapper():
        print('run hello')
        func()
    return wrapper

def hello():
    print('Hello World!')

hello = log(hello)
hello()

这段代码充分体现了后面所介绍的函数的个性,函数能够赋值给一个变量,函数能够当作参数传递个另一个函数,函数能够通过 return 语句返回函数。当初的 log 函数就是一个 装璜器
首先定义一个 log 函数,它接管一个函数作为参数,并且它的外部又定义了一个 wrapper 函数,wrapper 函数在打印日志当前,调用了传递进来的 func 函数(也就是 hello 函数),在 log 函数的最初返回这个外部定义的函数。
在示例代码的最底部,咱们将 hello 函数当作参数传递给 log 函数,并将其返回后果又赋值给变量 hello,此时的 hello 变量所指向的其实曾经不是原来的 hello 函数,而是 log 装璜器返回的外部函数 wrapper
当初调用方无需批改调用形式,依然应用 hello() 的形式去调用 hello 函数,但它的性能曾经加强了,会主动在执行 print('Hello World!') 逻辑之前加上打印日志的性能。
下面的代码咱们从性能上实现了 装璜器 的成果。但实际上,Python 在语法层面上间接反对了装璜器模式。仅须要一个 @ 符号就能让下面的代码更加可读,且易于保护。

def log(func):
    def wrapper():
        print('run hello')
        func()
    return wrapper

@log
def hello():
    print('Hello World!')

hello()

@ 符号是 Python 在语法层面上提供的语法糖,但它实质上齐全等价于 hello = log(hello)
以上就是一个最精简的合乎 Pythonic装璜器,无论你当前遇到如许简单的装璜器,请记住,它最终的实质实际上就是一个函数,只不过利用了一些 Python 中的函数个性使其可能解决更简单的业务场景。

被装璜的函数带有参数、返回值的装璜器

理论工作场景,咱们写的函数往往都很简单,想要写一个通用性更强的装璜器,还须要做一些细节局部的工作。不过你曾经理解了装璜器的实质,剩下的例子了解起来并不会很费劲,你只须要在特定的场景应用特定性能的装璜器就能够了。

def log(func):
    def wrapper(*args, **kwargs):
        print('run hello')
        return func(*args, **kwargs)
    return wrapper

@log
def hello(name):
    print('Hello World!')
    return f'I am {name}.'

result = hello('xiaoming')
print(result)

*args, **kwargs 这两个不定长参数,就很好的解决了装璜器通用性的问题,使得装璜器在装璜任何函数的时候,参数都能够原样的传入到原函数外部。wrapper 函数最初调用 func 函数的后面加上了 return 语句,它的作用就是将原函数的 return 后果返回给调用方。

放弃被装璜函数的元信息的装璜器

log 装璜器外部的 wrapper 函数打印日志的代码 print('run hello') 是固定的字符串,如果咱们想要让其能够依据函数名主动更改打印后果,如 print(f'run {函数名}.') 这样的模式。
每个函数都有一个 __name__ 属性,可能返回其函数名:

def hello(name):
    print('Hello World!')

print(hello.__name__)  # hello

但问题是当初应用了 log 装璜器当前,原来的 hello 函数曾经指向 wrapper 函数了,所以如果你测试就会发现,被装璜过的 hello 函数 __name__ 属性曾经变成了 wrapper,这显然不是咱们想要的后果。
咱们能够通过 wrapper.__name__ = func.__name__ 一行语句解决这个问题,不过咱们还有更好的方法。Python 内置了一个装璜器 functools.wraps 就可能帮咱们解决这个问题。

from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f'run {func.__name__}')
        return func(*args, **kwargs)
    return wrapper

@log
def hello(name):
    print('Hello World!')
    return f'I am {name}.'

print(hello('xiaoming'))
print(hello.__name__)

装璜器本身带有参数

兴许你想管制 log 装璜器的日志级别,那么给装璜器传参是一个很容易想到的方法,上面来看一下须要接管参数的装璜器的例子:

from functools import wraps

def log(level):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if level == 'warn':
                print(f'run {func.__name__}')
            elif level == 'info':
                pass
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log('warn')
def hello(name):
    print('Hello World!')
    return f'I am {name}.'

result = hello('xiaoming')
print(result)

和之前的装璜器相比,带参数的装璜器又多了一层函数嵌套,实际上成果是这样的 hello = log('warn')(hello),首先调用 log('warn') 返回的是外部 decorator 函数,接着就相当于 hello = decorator(hello),实际上到这一步就和不带参数的装璜器一样了。

装璜器即反对带参数又反对不带参数

有时候可能会遇到更加变态的需要,须要装璜器传不传参数都可能应用,解决形式有多种,我这里给出一个比较简单容易了解的实现。

from functools import wraps

def log(level):
    if callable(level):
        @wraps(level)
        def wrapper1(*args, **kwargs):
            print(f'run {level.__name__}')
            return level(*args, **kwargs)
        return wrapper1
    else:
        def decorator(func):
            @wraps(func)
            def wrapper2(*args, **kwargs):
                if level == 'warn':
                    print(f'run {func.__name__}')
                elif level == 'info':
                    pass
                return func(*args, **kwargs)
            return wrapper2
    return decorator

@log('warn')
def hello(name):
    print('Hello World!')
    return f'I am {name}.'

@log
def world():
    print('world')

print(hello('xiaoming'))
world()

callable 能够判断传递进来的参数是否可调用,不过须要留神,callable 只反对 Python3.2 及以上版本,你能够查看官网文档获取详细信息。

类装璜器

相比函数装璜器,类装璜更灵便,也更弱小。在 Python 类中能够定义 __call__ 办法,使其在无需实例化的状况下本身能够被调用,而此时就会执行 __call__ 外部的代码。

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

    def __call__(self):
        print('before')
        self._func()
        print('after')

@Log
def hello():
    print('hello world!')

hello()

装璜器装璜程序

一个函数其实能够同时被多个装璜器所装璜,那么多个装璜器的装璜程序是怎么的呢?上面咱们就来摸索一下。

def a(func):
    def wrapper():
        print('a before')
        func()
        print('a after')
    return wrapper

def b(func):
    def wrapper():
        print('b before')
        func()
        print('b after')
    return wrapper

def c(func):
    def wrapper():
        print('c before')
        func()
        print('c after')
    return wrapper

@a
@b
@c
def hello():
    print('Hello World!')

hello()

以上代码运行后果:

a before
b before
c before
Hello World!
c after
b after
a after

多装璜的语法等效于 hello = a(b(c(hello)))。依据打印后果不难发现这段代码的执行程序。如果你理解过 Node.js 的 Koa2 框架的中间件机制,那么你肯定不会生疏以上代码的执行程序,实际上 Python 装璜器同样遵循 洋葱模型 。多装璜器的代码执行程序就像剥洋葱一样,先由外到内进入,而后再由内到外。
给大家留一个思考题:最终的 hello.__name__ 指向哪一个装璜器外部的 wrapper 函数呢?

装璜器实战

了解了装璜器,咱们就要用起来,文章结尾有提到装璜器的用处,上面咱们来看一个理论场景下应用装璜器的例子。
Flask 是 Python Web 生态中十分风行的一个微框架,你能够到 GitHub 上查看其源码。上面就是一个用 Flask 编写的最小 Web 利用。
在这里 @app.route("/") 装璜器的作用就是将根路由 /发送过去的申请绑定到处理函数 hello 下面来进行解决。这样当咱们启动 Flask Web Server 当前,在浏览器地址拜访 http://127.0.0.1:5000/ 就可能取得返回后果 Hello, World!

from flask import Flask

app = Flask(__name__)

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

当然,更多的装璜器应用场景还是须要你本人亲自动手去摸索发现。

首发地址:https://jianghushinian.cn/

退出移动版