理解 Python 的装饰器, 看这一篇就够了!

1 – 简介

当我们执行某一些函数的时候, 可能会在其之前或者之后执行一些操作, 下面是一个常见的例子:

import time

def test ():
    n = 0
    for i in range (10000):
        n += i
    return n

t = time.time ()
test ()
spend = time.time() - t
print ("it took ", spend, "s")

上面这个例子实现了一个计时器, 用来统计函数执行的时间, 但是如果你需要在你的程序中大量复用这一个计时器的时候, 就会显得很复杂而且不可维护, 于是装饰器应运而生, 用来解决这一问题

说了这么多, 其实装饰器的作用就是帮助你在执行某些函数之前或者之后执行一些操作.

2 – 前置知识

2 – 1 闭包

关于闭包的详细知识: https://zhuanlan.zhihu.com/p/21680710

如果你不了解闭包也没有太大的问题, 你只需要知道一件事情:

当一个函数(f) 返回了其子函数对象(g) 的时候, 那么这个函数(f)的内置变量会被保存.

例子:

def foo():
    x = 5

    def inner():
        nonlocal x
        x += 1
        return x
return inner

p = foo()

在上面的例子中, inner 被其父函数 foo 返回, 所以 foo 中的变量会被保存, 以至于 inner 可以访问到 foo 中的变量

2 – 2 变长参数

详细知识请查看: https://www.runoob.com/python3/python3-function.html

例子:

def a (**kwargs):
    print (kwargs)

args = {
    "abc": 1,
    "def": 2
}
a (name="dog", **args)

>>> {'name': 'dog', 'abc': 1, 'def': 2}

我们定义了一个函数 a , 但是 a 可能会有很多个参数, 所以使用 两个星号(**) 定义一个用来存放参数的字典kwargs, 无法被固定参数接受的其他参数就会被存放到 kwargs

3 – 手动实现一个装饰器

# 计算运行时间的装饰器例子
import time
def decorater (func):
    def wapper ():
        t = time.time ()
        func ()
        print ("it took about ", time.time ()-t , "s")

    return wapper

def test ():
    n = 0
    for i in range (1000000):
        n += i
    return n
# 此方法实现了函数可复用.
test = decorater (test)

本例子的核心在于 test = decorater (test) 这一句, 这一句代码把原来的 test 函数指向 decorater 返回的 wapper 函数, 在此之后当你调用 test, 实际上你调用的是 wapper

4 – 用语法糖@ 定义装饰器

通过上面的例子, 我们已经大致了解装饰器的作用和流程

python 提供了一个语法糖 “@” 来定义一个装饰器, 使得代码看起来更加优雅可读.

import time
def decorater (func):
    def wapper (**kwargs): # 如果你的 fucn 函数需要传入参数, 请定义一个 **kwargs 参数.
        t = time.time ()
        func (**kwargs) # 将获取的参数传入原函数 , 如果你不知道为什么要这么写, 跟着我写就行了.
        print ("it took about ", time.time ()-t , "s")

    return wapper

@decorater
def test (n=0):
    for i in range (1000000):
        n += i
    return n

test ()

在本例中, “@” 符号所起的作用等同于 test = decorater (test)

5 – 总结

当你使用 @ 语法糖定义一个装饰器的时候, 它大概会执行这么几件事:

  1. 获取被装饰的函数 func
  2. 装饰符(), 返回对象 deco
  3. func = deco (func)

6 – 带参数的装饰器

假设我现在要做一个统计函数运行时长的装饰器, 但是我需要获取这个函数所处的线程或者其他信息, 如果可以在定义装饰器的时候把线程信息作为参数传入岂不是更好?

def get_time (thread_name:str = "main"):
    def decorater (func):
        def wrapper (**kwargs):
            t = time.time ()
            func (**kwargs)
            print (thread_name ," took about ", time.time ()-t , "s")
        return wrapper
    return decorater

@get_time ("test")
def test ():
    n = 0
    for i in range (1000000):
        n += i
    return n

我们按照步骤来解析上面的代码

1 – 获取被定义的函数 func:
这里的 func 指的就是 test

2 – 装饰符(), 返回对象 deco:
装饰符是 get_time ("test") 那么执行装饰符实际上就是 get_time ("test")()首先装饰符 get_time(“test”) 被执行, 返回 decorater , 那么这里的 对象 deco 指的就是 decorater

3 – func = deco (func)
test = decorater (test)
decorater (test) 返回了一个 wrapper 函数, 这个 wrapper 函数将会用来替换原有的 test

7 – 多个装饰器

@deco1
@deco2
def a ():
    ...

在上面的例子中, 装饰的顺序依次是 deco2 -> deco1

理论上不需要对装饰器函数做任何更改即可支持多重装饰器.

8 – 拓展: Flask 中的 @app.route ()

用我们刚才所学的知识, 我们尝试来理解 Flask 的路由定义代码.

def route(rule):
    def decorator(f):
        add_url_rule(rule, f)
        return f

    return decorator

@route ("/")
def index ():
    return "Hello world"

上面是对 flask app.route() 的一个简化抽象: flask 实际上是使用了函数 add_url_rule 用来添加路由, 使用装饰器语法糖来定义路由只是让代码看起来更加可读好看.

利用我们刚才学的三部曲解析, 当 index 在被装饰的过程中调用了 add_url_rule(rule, f) 来将url添加到 Flask 的路由树中, 随后返回被装饰的函数本身, 也就是是说, 被装饰过后的 index 还是原来的 index , 在此处使用装饰器只是变相地调用了 add_url_rule

Leave a Reply