Python 装饰器入门(上)

翻译前想说的话:

  这是一篇介绍python装饰器的文章,对比之前看到的类似介绍装饰器的文章,个人认为无人可出其右,文章由浅到深,由函数介绍到装饰器的高级应用,每个介绍必有例子说明。文章太长,看完原文后我计划按照文章作者的划分,将分为两章翻出来和大家分享,如果你觉得干的还不错,就点个赞吧.

 

目录:

    • 函数
      • 一等对象
      • 内部函数
      • 从函数中返回函数
    • 简单装饰器
      • 语法糖
      • 复用装饰器
      • 装饰器传参
      • 从装饰器返回值
      • 你是谁?
    • 一些现实中的例子
      • 时间函数
      • 调试代码
      • 给代码降速
      • 注册插件
      • 用户是否登录?
    • 有想象力的装饰器
      • 装饰类
      • 嵌套的装饰器
      • 带参数的装饰器
      • Both Please, But Never Mind the Bread 这句话开始我不知道怎么翻,直到我看到了维尼熊......,请在这里www.google.com检索Winnie the Pooh  Both Please, But Never Mind the Bread
      • 有状态的装饰器
      • 类装饰器
    • 更多现实中的例子
      • 代码降速,重新访问
      • 创建单例模式
      • 缓存返回值
      • 添加单元信息
      • 验证JSON

 正文开始:

在本次的装饰器教程中,将介绍何为装饰器以及如何创建和使用它们,装饰器提供了简单的语法来调用高阶函数。

从定义上讲,装饰器是一个函数,它接收另一个函数作为参数并且扩展它的功能,但不会显式的去修改它

说起来可能会让人觉得难理解,但它(装饰器)确实不会这么做,特别是一会你会看到一些装饰器如何工作的例子

 函数

在理解装饰器之前,你首先需要理解函数如何工作。函数会基于给定的参数返回值。这里有一个非常简单的例子:

>>> def add_one(number):
...     return number + 1

>>> add_one(2)
3

通常情况下,函数在python中也会有其它功效而不是仅仅接收输入并返回输出。print()函数是一个例子。在控制台输出的时候它会返回None(1),然而,为了理解装饰器,
将函数认为是接收参数并返回值就足够了

注意:在面向函数编程,你几乎只会使用纯函数,不会有其它功能,然而python不是一个纯函数式语言,python支持许多函数式编程概念,包括一等对象

 一等对象

在python中,函数是一等对象,意思是函数可以作为参数被传递,就像其它的对象(string,int,fload,list和其它),思考下面的三个函数

def say_hello(name):
    return f"Hello {name}"

def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def greet_bob(greeter_func):
    return greeter_func("Bob")

在这里,say_hello()和be_awsone()是常规函数,接收一个name参数返回一个字符串,然而greet_bob()函数,接收一个函数作为他的参数,我们可以将say_hello()或者be_awesome()函数传递给它

>>> greet_bob(say_hello)
'Hello Bob'

>>> greet_bob(be_awesome)
'Yo Bob, together we are the awesomest!'

注意greet_bob(say_hello) 涉及到两个函数,但是不同的是:greet_bob()和say_hello,say_hello函数并没有使用(),代表只传递了对函数的引用,函数没有运行,greet_bob()函数,是使用了括号,所以它会被正常调用

 内部函数

在函数内定义函数是被允许的。这类函数被称为内部函数,这里有一个函数和两个内函数的例子

def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()

当你调用parent()的时候会发生什么? 请考虑一分钟。会出现下面的输出结果

>>> parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function

注意内部函数定义的顺序无关紧要,和其它的函数一样,打印只会发生在内部函数运行的时候

而且,内部函数在父函数被调用之前不会生效,它们的局部作用域是父(),它们只作为局部变量存在在父()函数的内部,尝试调用first_child(),你会得到下面的错误

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'first_child' is not defined

不管你何时调用parent(),内部函数first_child()和second_child()都会被调用,因为它们的局部作用域,它们无法再parent()函数外使用

从函数中返回函数

python允许使用函数来作为返回值,下面的例子从外部的父函数parent()返回了一个内部函数

def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

    if num == 1:
        return first_child
    else:
        return second_chil

注意这里返回的first_child是没有括号的,也就是返回了对函数first_child的引用, 带括号的first_child() 指的是对函数求值的结果,这个可以在下面的实例中看到

>>> first = parent(1)
>>> second = parent(2)

>>> first
<function parent.<locals>.first_child at 0x7f599f1e2e18>

>>> second
<function parent.<locals>.second_child at 0x7f599dad5268>

这个输出代表first变量引用了在parent()中的本地函数first_child(),second则指向了second_child()

你现在可以像常规函数一样使用first和second,虽然他们指向的函数无法被直接访问

>>> first()
'Hi, I am Emma'

>>> second()
'Call me Liam'

请注意,在前面的例子中我们在父函数中运行内部函数,例如first_child(),然后在最后的例子中,返回的时候没有给内部函数first_child添加括号。这样,就获取了将来可以调用的函数的引用。这样有意义吗?

简单装饰器

现在你已经看到函数和python中的其它对象一样,你已经准备好前进来认识python装饰器,让我们以一个例子开始:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

你能猜到当你调用say_whee()的时候回发生什么么?试一下:

>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

要理解这里发生了什么,需要回看下之前的例子,我们只是应用了你到目前为止学到的所有东西

所谓的装饰器发生在下面这行:

say_whee = my_decorator(say_whee)

事实上,say_whee现在指向了内部函数wrapper(),当你调用my_decorator(say_whee)的时候会将wrapper作为函数返回

>>> say_whee
<function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>

wrapper()引用原始的say_whee()作为func,在两个print()之间调用这个函数

简而言之:装饰器包裹一个函数,并改变它的行为

在继续之前,让我们看下第二个例子。因为wrapper()是一个常规的函数,装饰器可以以一种动态的方式来修改函数。为了不打扰你的邻居,下面的示例演示只会在白天运行的装饰器

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)

如果你在睡觉的时间调用say_whee(),不会发生任何事情

>>> say_whee()
>>>

语法糖

上面的装饰器say_whee()用起来有一点笨拙。首先,你键入了三次say_whee,另外,装饰器隐藏在了函数的定义之下

作为替代,python允许你使用@symbol的方式使用装饰器,有时被称为"pie"语法,下面的例子和之前第一个装饰器做了同样的事情

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_whee():
    print("Whee!")

所以,@my_decorator 只是say_whee = my_decorator(say_whee)的一种快捷方式,这就是如何将装饰器应用到函数上

复用装饰器

回想一下,装饰器只是一个普通的函数。所有常用的工具都是方便重复利用的,让我们将装饰器移动到他自己的模型上以便于在其它的函数上使用

下面创建了一个decorators.py

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice

注意:你可以随意定义内部函数的名称,通常像wrapper()用起来是没问题的。你在这篇文章中会遇到许多装饰器。为了区别开它们,我们将使用decorator名称来命名内部函数,但会加上wrapper_前缀。

你可以使用常规导入来使用一个新的装饰器

from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")

当你运行这个例子,你会看到原始韩式say_whee()执行两次

>>> say_whee()
Whee!
Whee!

装饰器传参

如果你有一个函数需要接收一些参数,这时候还可以再使用装饰器么,然我们试试

from decorators import do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")

不幸的是,运行代码抛出了错误

>>> greet("World")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

问题在于内部函数wrapper_do_twice()没有接收任何参数,但是name="World"却传给了它。你可以让wrapper_do_twice()接收一个参数来修补这个问题,但是这样前面的say_whee()函数就无法工作了

解决方案是在内部函数使用*args和**kwargs ,这样它会允许接收任意个关键参数,下面重写了decorators.py

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

内部函数wrapper_do_twice()现在接收任意数量的参数并会传递给装饰的函数,目前say_whee()和greet()都会正常工作

>>> say_whee()
Whee!
Whee!

>>> greet("World")
Hello World
Hello World

从装饰器返回值

被装饰的函数返回值会发生什么?这会由装饰器来决定,我们下面有一个简单的装饰器函数

from decorators import do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

尝试运行它:

>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
None

装饰器吃掉了从函数返回的值

因为do_twice_wrapper()没有返回值,调用 return_greeting("Adam") 最后返回了None

修复的方式是,需要确认装饰器返回它装饰的函数的值,改变decorators.py文件:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

执行这个函数返回的值:

>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

你是谁?

在使用Python(尤其是在交互式shell中)时,强大的内省是非常方便的功能。内省是对象在运行时了解其自身属性的能力。例如,函数知道自己的名称和文档:

>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:

print(...)
    <full help message>

内省同样适用于你自定义的函数:

>>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_whee.__name__
'wrapper_do_twice'

>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice()


然而在被装饰后,say_whee()会对自身感到疑惑。它现在显示为 do_twice()装饰器的内部函数 wrapper_do_twice()
    
为了修复这个,装饰器需要使用@functools.wraps装饰器,它会保留原始函数的信息,再次更新下decorators.py:

import functools

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

不需要对被装饰的say_whee()函数做任何更改:

>>> say_whee
<function say_whee at 0x7ff79a60f2f0>

>>> say_whee.__name__
'say_whee'

>>> help(say_whee)
Help on function say_whee in module whee:

say_whee()


非常好,现在say_whee()在被装饰后可以保持自己

技术细节:@funtools.wraps 装饰器使用函数functools.update_wrapper()来更新指定的属性,像__name__和__doc__来用于自省

 一些现实中的例子

让我们看一些用处更大的装饰器例子。你会注意到他们主要的模式和你现在所学的都是一样的

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

对于构建更复杂的装饰器,这个是一个很好的模板

时间函数

让我们从@timer装饰器开始,它会测量函数运行的时间并且打印持续时间到控制台,这是代码:

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

这个函数是在函数运行之前获取时间(#1行),并且在函数运行结束之后获取时间(#2行),我们使用 time.perf_counter() 函数,这个函数可以非常好的计算时间间隔。下面是一个示例:

>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs


自己运行测试下,手敲下这里的代码,确保你理解它的工作原理。如果不明白,也不要担心。装饰器是高级方法,试着思考下或者画下流程图

注意: 如果你只是想获取函数的运行时间,@timer 装饰器可以满足。如果你想获取到更精确的数据,你应该考虑使用timeit 模块来替代它。它临时禁用了垃圾收集并且运行多次以避免函数快速调用带来的噪音数据

调试代码

下面的@debug函数会在每次调用的时候打印函数被调用的参数和它的返回结果

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

signature 变量是通过 字符串表示方法 来创建所有的输入参数。下面的数字对应了代码中的注释
    1、将args创建为列表,使用repr修饰
    2、将kwargs创建为列表,使用f-string格式化参数为key=value,!r表示使用repr()表示值
    3、args和kwargs转换后会合并在signature变量中,使用逗号分隔每个变量
    4、函数运行结束后会返回值

让我们在一个简单的函数中使用装饰器被观察它是如何运行的,被装饰的函数只有一个位置参数和一个关键字参数

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"

注意@debug装饰器如何打印make_greeting()函数的signature 和返回值

>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'

>>> make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Whoa Richard! 112 already, you are growing up!'

>>> make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)
'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
'Whoa Dorrisile! 116 already, you are growing up!'     

@debug修饰符看起来只是重复了我们刚才写的内容 ,并不是非常有用。 但当应用到不能直接修改的其它函数时,它会更加强大。

下面的例子计算了一个数学常数E的近似值

import math
from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))

这个例子还演示了如何将装饰器应用到已经定义了的函数

当调用approximate_e()函数,你可以看到@debug函数在工作:

 

>>> approximate_e(5)
Calling factorial(0)
'factorial' returned 1
Calling factorial(1)
'factorial' returned 1
Calling factorial(2)
'factorial' returned 2
Calling factorial(3)
'factorial' returned 6
Calling factorial(4)
'factorial' returned 24
2.708333333333333

在这个例子中,可以得到一个真实值的近似值e = 2.718281828

给代码降速

下面的例子看起来可能不是很有用。可能最常见的用例是,您希望对一个不断检查资源是否存在的函数进行速率限制 。 @slow_down decorator在调用被修饰的函数之前会暂停一秒钟

import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)       

来看下@slow_down装饰器的效果,你需要自己运行跑下

>>> countdown(3)
3
2
1
Liftoff!   

countdown()是一个递归函数。也就是说,它是一个调用自身的函数 。     

注册插件

装饰器不是必须要修饰被装饰的函数(这句话不太好翻译,看下面的例子理解起来很容易),它还可以简单地注册一个函数,并将其解包返回,例如,可以使用它来创建一个轻量级插件体系结构:

import random
PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name) 

@register装饰器只是在全局PLUGINS 字典中储存了被装饰函数的引用。注意你不需要在例子中写内部函数或者使用@functools.wraps ,因为返回的是一个未经过修改的初始函数

randomly_greet()函数在注册函数中随机选择一个使用。注意PLUGINS字典已经包含了对注册为插件的每个函数对象的引用:

 >>> PLUGINS
{'say_hello': <function say_hello at 0x7f768eae6730>,
 'be_awesome': <function be_awesome at 0x7f768eae67b8>}

>>> randomly_greet("Alice")
Using 'say_hello'
'Hello Alice'

这个插件的主要用处在于不需要再单独维护一个插件列表。这个列表在插件注册时自动创建,使得添加一个新插件变得很简单,只需定义函数并用@register装饰即可。

如果你对python中的globals()函数熟悉,你可能会看到一些和我们的插件结构相似之处。globals()可以访问当前作用于的所有全局变量

包括我们的插件:

>>> globals()
{..., # Lots of variables not shown here.
 'say_hello': <function say_hello at 0x7f768eae6730>,
 'be_awesome': <function be_awesome at 0x7f768eae67b8>,
 'randomly_greet': <function randomly_greet at 0x7f768eae6840>}

使用@register 装饰器,可以创建感兴趣的变量管理列表,有效地从globals()中筛选出一些函数

用户是否登录?

在继续讨论一些更有趣的装饰器之前,让我们在最后一个示例中演示通常在处理web框架时使用的装饰器。在这个例子中,我们使用Flask去设置一个/secret web页面,这个页面只对登录用户或者其他有权限的用户展示

from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    @functools.wraps(func)
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
    ...    

虽然这里演示了如何对web框架添加身份验证吗,但通常不应该自己编写这些类型的装饰器。对于Flask可以使用Flask-login扩展,这里的功能更丰富也更加安全

有想象力的装饰器
到目前为止,你已经看到了如何创建简单的装饰器并且非常了解什么是装饰器以及它们是如何工作的。请从这篇文章中休息一下,练习学到的一切。

在本教程的第二部分中,我们将探索更高级的特性,包括如何使用以下特性:
    1、在类上使用装饰器(装饰类)
    2、在一个函数上应用多个装饰器
    3、带参数的装饰器
    4、可以选择是否接收参数的装饰器
    5、带状态的装饰器
    6、类装饰器

posted @ 2018-10-26 00:47  丁壮  阅读(2507)  评论(0编辑  收藏  举报