Python 到此不再有秘密系列(1)—深入浅出装饰器(decorators)(上)

521 阅读8分钟

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

为了分享此篇文章,个人做了大量的工作,所以未经本人同意,请勿转载,在此表示感谢!

在本次分享中,接下来将介绍什么是装饰器(decorators) 以及如何创建和使用装饰器。装饰器提供了一种调用高阶函数的简单语法。根据定义,装饰器是一个函数,接收函数并对函数的行为进行扩展,只是扩展并不改变函数原有行为。这听起来有点 confusing,但其实并不复杂,相信看过这边文章对装饰器会有全面深刻的认识,然后通过大量 example 巩固成果。

在正式开始之前我们通过一个小例子来感受一下装饰器带来好处,体验一下装饰器好处。

import time

def slow_square(number):
    print(f"Sleeping for {number} seconds")
    time.sleep(number)
    return number**2

print(slow_square(3))
print(slow_square(3))
print(slow_square(3))

上面代码执行后,用 sleep 模拟一个耗时操作,让线程休眠 3 秒钟。执行代码会每间隔 3 秒输出一个数字,其实这个函数输出结果都一样。

我们先不用关心具体@functools.lru_cache(maxsize=128, typed=False)参数含义,只要知道这个是装饰器,通过这个装饰器修饰上面函数slow_square后再次运行代码,效果只仅停留 3 秒后直接一口气输出三个 9。

import time
import functools

@functools.lru_cache(maxsize=128, typed=False)
def slow_square(number):
    print(f"Sleeping for {number} seconds")
    time.sleep(number)
    return number**2

print(slow_square(3))
print(slow_square(3))
print(slow_square(3))

我们输出一下functools.lru_cache,可以看到其实所谓装饰器就是一个函数。

print(functools.lru_cache) #<function lru_cache at 0x108fcd560>

函数(Functions)

在理解装饰器之前,需要做一些准备工作,首先理解函数是如何工作的。可以简单将函数,一个函数根据给定的参数返回一个值。下面是一个非常简单的例子。

def add(num_1,num_2):
    return num_1 + num_2

一般来说,Python 中的函数也可能有副作用,而不仅仅是把输入转换为输出。print() 函数就是一个典型的例子,该函数返回 None 的同时有一个向控制台输出的副作用,IO 操作可以看成一个副作用。然而,为了理解装饰器,只要把函数看作是把给定的参数变成一个值的东西就足够了。

注意:在函数式编程中,大部分函数都是没有副作用的纯函数。虽然 python 并不像 Haskell 不是一种纯粹的函数式语言,但 Python 支持许多函数式编程的概念,包括将函数作为第一类对象,这一点和 Javascript 相似函数在语言里都是一等公民。

函数式

在 Python 中,函数是第一类对象。这意味着函数可以像其他对象 (string, int, float, list, 等等) 一样作为参数传递和也可以作为返回值。

print_proxy = print

函数可以赋值给一个变量

def greet(name,printer=print):
    printer(f"hi {name}")

我们可以将 print 作为一个参数传递给 greet

def reverse(text):
    print(text[::-1])

greet("hi decorations",printer=reverse)

内嵌函数

我们可以在其他函数中定义内嵌函数,这样的函数被称为内部函数。下面函数 prefix_factory 定义一个内部函数 prefix_print

def prefix_factory(prefix):
    def prefix_print(text):
        print(f"{prefix}: {text}")
    return prefix_print

函数作为返回值

对于高阶函数而言,可以将函数做返回值返回。

def prefix_factory(prefix):
    def prefix_print(text):
        print(f"{prefix}: {text}")
    return prefix_print

简单的装饰器

现在你已经看到函数就像 Python 中的其他对象一样,好了我们已经对 python 函数有一定的认识了,现在就开始写一个 Python 装饰器。

def before_and_after(func):
    def wrapper(text):
        print("BEFORE")
        func(text)
        print("AFTER")

    return wrapper
def greet(text):
    print(f"hi {text}") 

greet = before_and_after(greet)
greet("decorators")
BEFORE
hi decorators
AFTER

语法糖

你上面装饰 before_and_after()的方式略显有些笨拙。显然不是什么优雅的方式来实现装饰器,出现了 3 次 greet函数名称。此外,装饰被隐藏在函数定义的下面。

不过 Python 允许你用 @ 符号以更简单的方式使用装饰器,这种方式就是我们熟悉语法糖的方式。下面就是通过语法糖对 greet 进行装饰,功能上和之前的函数完全相同。

def before_and_after(func):
    def wrapper(text):
        print("BEFORE")
        func(text)
        print("AFTER")

    return wrapper

@before_and_after
def greet(text):
    print(f"hi {text}")

greet("decorators")

重用装饰器

回顾一下,装饰器只是一个普通的 Python 函数。所有常用的便于重用的工具都是可用的。让我们把装饰器移到它自己的模块中,可以在许多其他函数中使用。

创建一个名为 decorators.py 的文件,内容如下。

import random

def do_twice(func):
    def wrapper(*args,**kwargs):
        val_1 = func(*args,**kwargs)
        val_2 = func(*args,**kwargs)
        return (val_1,val_2)
    return wrapper

@do_twice
def roll_dice():
    return random.randint(1,6)


print(roll_dice())

在使用 Python 时,特别是在交互式 shell 中,一个很大的便利就是它强大的自省能力。自省是指一个对象在运行时了解其自身属性的能力。例如,一个函数知道它自己的名字和文档。

>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:
def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        res_1 = func(*args, **kwargs)
        res_2 = func(*args, **kwargs)
        return res_1, res_2
    return wrapper_do_twice
@do_twice
def guess_number():
    return random.randint(1,6)
guess_number.__name__ #'wrapper_do_twice'
help(guess_number)

当函数guess_number() 被装饰后,guess_number()对自己的身份也随之发生了变化,这一点让人有点 consufing。当调用 guess_number.__name__ 查看函数身份时,输出不是 guess_number 而是 do_twice() 装饰器中的 wrapper_do_twice()内部函数。虽然并没有问题,不过我们希望返回的是 guess_number 而不是· wrapper_do_twice()

为了解决这个问题,可以装饰器应该使用 @functools.wraps 装饰器,这样就不会保留被装饰函数的信息。

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        res_1 = func(*args, **kwargs)
        res_2 = func(*args, **kwargs)
        return res_1, res_2
    return wrapper_do_twice
@do_twice
def guess_number():
    return random.randint(1,6)
guess_number.__name__ #'guess_number'

应用装饰器的一些例子

让我们来看看几个更有用的装饰器的例子。你会注意到,它们主要遵循你到目前为止所学到的相同模式。

计时器

def timer(func):
    @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 slow_square(number):
    time.sleep(number)
    return number**2
slow_square(3)
Finished 'slow_square' in 3.0003 secs
9

上面这个装饰器大家自己看代码,代码比较简单也容易理解,所以也就不做过多解释,装饰器就是会输出装饰函数的执行耗时时间。

注意:如果你只是想了解你的函数的运行时间,上面实现的 @timer 装饰器是很好的选择。如果想对代码执行时间进行更精确的测量,可能 @timer 就无法满足,应该考虑标准库中的 timeit 模块。暂时禁用垃圾收集,并运行多个试验来消除函数调用的 noise。

调试工具

接下来写一个调试装饰器,@debug 装饰器将在每次函数被调用时,打印函数的参数以及其返回值。

def debug(func):
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k,v in kwargs.items()]
        
        signatre = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signatre})")
        
        value = func(*args,**kwargs)
        print(f"{func.__name__!r} returned {value!r}")
        return value
    return wrapper_debug
  • 创建一个位置参数的列表,通过repr()得到一个表示每个参数的优雅字符串
  • 创建一个关键字参数的列表。f-string 将每个参数格式化为 key=value,其中的!r 指定符表示用 repr() 来表示值
@debug
def slow_square(number):
    time.sleep(number)
    return number**2
slow_square(3)
Calling slow_square(3)
'slow_square' returned 9
9

慢节奏

下面这个例子可能看起来不是很有用。为什么想 Python 代码慢下来呢?最常见的用例可能是,对一个连续检查资源例如查看下载进度,这时候这个方法就可能会派上用场。

def slow_down(func):
    @functools.wraps(func)
    def wrapper_slow_donw(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_donw
@slow_down
def countdown(from_num):
    if from_num < 1:
        print("done")
    else:
        print(from_num)
        countdown(from_num - 1)

注册机

Decorators don’t have to wrap the function they’re decorating. They can also simply register that a function exists and return it unwrapped. This can be used, for instance, to create a light-weight plug-in architecture:

装饰者不一定要包装他们所装饰的函数。也可以简单地注册一个函数到集合,然后直接返回函数。例如,这可以用来创建一个轻量级的插件架构。

PLUGINS = dict()

def register(func):
    PLUGINS[func.__name__] = func
    return func

@register
def java_dev(name):
    return f"my name is {name} and I am Java Developer"

@register
def python_dev(name):
    return f"my name is {name} and I am Python Developer"

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

@register 装饰器只是在全局 PLUGINS dict中存储了一个对被装饰函数的引用。注意,在这个例子中,不必写一个包装函数或使用 @functools.wraps,因为返回的是未经任何修改的原函数

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

randomly_greet("mike")
Using 'java_dev'
'my name is mike and I am Java Develope