这是我参与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