【翻译】深入浅出Python装饰器之基本用法

為{幸葍}努か 提交于 2020-01-15 15:57:15

深入浅出Python装饰器

前言

前两天被问到装饰器的概念,在我的理解里:装饰器就是一个函数,他的参数为函数,一般用来对参数进行一些额外的处理,但不影响参数函数的表现。

但说实话写的不多,于是就想查一下资料具体看一下。而前两天发现谷歌设置为中英文,搜索结果的排序都不一样。于是就翻了下英文的资料,看完之后感觉讲的的确不错,就诞生了想尝试翻译一下的想法,于是就有了本文。

老外讲东西太细了,所以有些显而易见的话会被我过滤掉……另外,高级用法暂时未翻。

本文翻译自Primer on Python Decorators的第一部分,深入浅出是个人翻译,Primer原义为入门读物,所以也可以翻译为Python装饰器入门指南。

翻译正文

目录

本文的结构如下:

  • 函数
    • 第一类对象(First-Class Objects)
    • 内置函数
    • 返回值为函数的函数
  • 简单的装饰器
    • 语法糖
    • 一个重复使用的装饰器
    • 带参数的装饰器函数
    • 装饰器函数的返回值
    • 你是谁啊?(函数名)
  • 一些真实用例
    • 计时函数
    • 代码调试
    • 减速/延迟执行的代码
    • 插件注册
    • 检查用户是否登陆的装饰器
  • 华丽的装饰器/装饰器的高级用法
    • 装饰类
    • 多重装饰器
    • 带参装饰器
    • 多功能装饰器(带参不带参均可)
    • 含有状态的装饰器
  • 另外一些真实用例
    • 减速/延迟执行的代码
    • 单例模式的实现
    • 缓存返回值
    • 向单元里添加信息
    • 验证JSON是否合理
  • 总结
  • 深入阅读

另:此文有配套视频

前言

在这个教程里,我们将会展示怎样创建和使用装饰器。装饰器使用了一个很简单的语法,叫高优先级函数(higer-order functions,简单说就是参数包含函数,返回值是函数)。

从定义上来说,装饰器是一个以另外一个函数为参数,对参数函数进行扩展但并不明显改变参数函数。

听上去比较绕口,但当你看到一些例子之后,会发现其实挺简单的。你可以在github找到一些例子。

函数

要想理解装饰器,就要先搞清楚函数怎么运行。一般来讲,函数根据参数返回一个值,如下面的例子:

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

>>> add_one(2)
3

不过并非所有函数都会根据输入提供输出,比方说print()函数,不反回任何值,只是将参数打印到控制台。不过为了理解装饰器,考虑有参数/返回值的函数就够了。

第一类对象

在Python中,函数属于第一类对象。这意味着函数可以像参数一样传递,也可以像使用参数一样来使用,跟其他对象(string,int,float等等)没什么区别。看下面的函数例子:

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")

在这个例子里,前两个函数就是常见的类型。而最后一个函数,参数是一个函数。也就是说,我们可以把前两个函数当参数传给第三个函数,如下:

>>> 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 。区别是后者没有带括号。也就是说后者被当作参数引用了,但并未执行,前者就是常见的普通函数调用。

内置函数(Inner Functions)

Python中可以在函数内定义函数,这被称为内置函数。下面是两个例子:

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()
  
# output
>>> parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function

可以发现,执行结果的顺序和定义的位置无关,打印的逻辑也是在被调用的时候执行。

进一步说,内置函数,在父函数被执行前都还未定义。同时,他们也仅存在于父函数内部(类似于局部变量)。意味着,如果在外部调用子函数,会报错。

返回值是函数的函数

Python允许将函数返回值设置为函数,如下:

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_child     

注意,两个返回值都不带括号,这代表父函数返回的是子函数的引用。若返回值带括号,则是返回子函数的执行结果。可以通过下面的办法验证:

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

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

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

返回值代表:first变量是父函数的本地函数first_child()的一个引用,second同理。所以,可以使用first()和second()来执行:

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

>>> second()
'Call me Liam'

考虑一下,这个例子中父函数的返回值为什么不带括号,而inner functions那里的例子带括号,有什么区别?后者有什么意义?

答案:如果带括号,则返回值不确定,返回值为内置函数的返回值;不加括号,则是返回一个函数

简单的装饰器

直接上例子:

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)

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

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

父函数的返回值是内置函数wrapper,say_whee是前者的一个引用,参数为另外一个函数。执行say_whee(加/不加括号)表现不同。

简单说,装饰器包裹了一个函数,并改变了他的表现。

还可以做一些额外的控制,比方说控制执行时间:

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,有些繁琐。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

# @my_decorator 等价于 say_whee = my_decorator(say_whee)
@my_decorator
def say_whee():
    print("Whee!")
装饰器的重复使用

此节只是将可以把装饰器封装到一个包里,在其他地方复用。

比方说可以把下面函数写到一个名叫 decorators.py的文件里,在其他地方引用。

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice
from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")
    
>>> say_whee()
Whee!
Whee!
带参装饰器函数

继续使用上一个例子,如果传递一个参数进去,会怎样?

from decorators import do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")
    
# output
>>> 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()不接受任何参数,但是实际调用过程中又把World传入了。最直观的办法是,给wrapper_do_twice()赋参数,但如果参数并不叫name其他地方就不能使用了。

一劳永逸的办法是使用 *args和**kwargs,如下:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice
    
>>> 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

并没有打印出 'Hi adam',问题出在 do_twice_wrapper() 函数,因为他没有返回值,只是简单的执行了被装饰得函数。可以做如下改动:

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终端,会提供一个特别好用的功能:反省(introspection),即可以了解到对象的一些特征,比方说函数名。

>>> 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()

可以看到,我们上文使用的例子表现有些不对了。可以使用一个Python自带的装饰器来调整一下:

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

# output
>>> 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()

一些真实的例子

可能你已经注意到了,基本上装饰器都是下面的套路:

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
计时装饰器

下面是一个计算函数运行时间的装饰器实现:

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)])

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

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs
调试代码的装饰器

下面的代码实现了一个可以打印被装饰函数的各个参数和返回值的装饰器

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

实际使用:

@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!"
        
        
>>> 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!'
插件注册

装饰器并非一定要对被装饰函数进行处理,比方说可以用来进行简单的插件注册(很晦涩,直接看例子):

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)

使用:

>>> 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自己也会使用类似的方法,比如 globale() 函数,调用可以看到已经注册的一些变量:

>>> 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>}
用户是否登陆

现实开发中,装饰器使用最多的一个场景是在web框架中。比如Flask下,我们可以使用一个装饰器,来判断用户是否已经登陆:

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():
    ...

当然,也可以选用框架自带的插件,他们的安全性和可用性会更好。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!