在 Python 中使用装饰器的 7 个层次(7 Levels of Using Decorators in Python)
文章目录
- [在 Python 中使用装饰器的 7 个层次(7 Levels of Using Decorators in Python)](#在 Python 中使用装饰器的 7 个层次(7 Levels of Using Decorators in Python))
-
- 导言
- [Level 0: 了解基本概念Basic Concepts和用法Usages](#Level 0: 了解基本概念Basic Concepts和用法Usages)
- [Level 1: Wrap a Function](#Level 1: Wrap a Function)
- [Level 2: 将多个装饰器应用于一个函数](#Level 2: 将多个装饰器应用于一个函数)
- [Level 3: 包装接收参数的函数](#Level 3: 包装接收参数的函数)
- [Level 4: 编写可接收参数的装饰器](#Level 4: 编写可接收参数的装饰器)
- [Level 5: 保留原始函数Original Functions的元数据Metadata](#Level 5: 保留原始函数Original Functions的元数据Metadata)
- [Level 6: 保持简单 --- 装饰器的设计哲学](#Level 6: 保持简单 — 装饰器的设计哲学)
掌握 Python 最神奇的功能
导言
在技术面试中,区分初级junior和高级senior Python 程序员的最简单、最快的方法就是让他或她编写装饰器decorator。因为掌握装饰器decorator这个最神奇的 Python 特性是 Python 开发者的一个里程碑milestone。
关于装饰器decorators有很多值得一提的提示tips和技巧tricks,但它们分散在不同的书籍或教程tutorials中,其中一些可能会让初学者更加困惑。这就是我写这篇文章的原因。
本文将分 7 个层次levels深入探讨 Python 装饰器decorators的所有核心概念core concepts、技术和用法。如果您理解了其中的一半,那么阅读包含装饰器的 Python 程序就会变得容易。如果您理解了全部,那么在 Python 中设计和编写装饰器将是小菜一碟piece of cake。
Level 0: 了解基本概念Basic Concepts和用法Usages
什么是装饰器decorator?
装饰器decorator只是 Python 的一个函数式编程特性functional programming feature。装饰器decorator接收一个可调用对象callable(函数function、方法method或类class),并返回一个可调用对象callable。让我们来看一个简单的例子:
python
def add_author(func):
print('Zhang San')
return func
@add_author
def get_title():
return '7 Levels of Using Decorators in Python'
print(get_title())
# Zhang San
# 7 Levels of Using Decorators in Python
上述 add_author(func)
函数是一个简单的装饰器decorator。它在接收函数运行前打印作者姓名。
如果函数需要使用该装饰器,我们可以在函数顶部的"@"符号后添加该装饰器。
"@"符号的使用乍一看让人一头雾水,但这只是 Python 的语法糖syntax sugar。我们还可以如下应用装饰器:
python
get_title = add_author(get_title)
上述方法的结果与"@"方法完全相同:
python
def add_author(func):
print('Zhang San')
return func
def get_title():
return '7 Levels of Using Decorators in Python'
get_title = add_author(get_title)
print(get_title())
# Zhang San
# 7 Levels of Using Decorators in Python
因此,"@"方法一点也不吓人。它只是为我们提供了一个非常直观、优雅的选择,可以将装饰器应用到函数中。
我们为什么需要装饰器decorator?
装饰器decorator就像一个可重复使用的构件building block,我们可以在需要时将其应用到函数中,而无需编辑函数本身。
如前面的示例所示,只要我们需要在 get_title()
函数前打印作者姓名,就可以直接将该代码块( add_author
装饰器)装配到函数中。无需对 get_title()
函数进行任何修改。如果将来不需要打印作者姓名,只需删除或注释掉 get_title()
顶部的一行即可。
请牢记:"多次编辑一个函数容易出错。而在需要时组装装饰器则既优雅又不会出错。"
简而言之In a nutshell,装饰器decorator为我们提供了很大的灵活性,并将功能方法和主方法分离开来。许多 Python 内置模块和流行的第三方模块都使用了这一强大功能。
Level 1: Wrap a Function
在某些资料中,装饰器decorator也被称为包装器wrapper。因为它可以包装wrap一个函数function并改变其行为。
至于 level 0 中的示例,我们的装饰器decorator只是在 get_title()
执行之前打印了一些内容。我们能做得更多吗?例如更改 get_title()
返回的标题?
当然,如下所示:
python
def add_things(func):
def wrapper():
title = func()
new_title = title + ' !!!'
return new_title
return wrapper
@add_things
def get_title():
return '7 Levels of Using Decorators in Python'
print(get_title())
# 7 Levels of Using Decorators in Python !!!
如上所示,我们定义了一个名为 wrapper()
的内部函数inner function,该函数封装了接收到的 func
并在其结果末尾添加了三个感叹号。
基本上,这个示例展示了在 Python 中编写装饰器decorator的通用模板common template。共有 3 个步骤:
- 接收函数function作为参数argument
- 定义一个包装函数wrapper function,对接收到的函数进行处理
- 返回包装函数wrapper function
顺便提一下,在函数式编程functional programming中,我们将包含嵌套函数的装饰器命名为闭包closures。
到目前为止,我们已经了解了装饰器decorators的基本原理。我们还能编写一些简单的装饰器。
遗憾的是,实际需求可能非常复杂,上述基础知识不足以设计出一个强大的装饰器decorator。接下来将介绍更高级的装饰器技术。
Level 2: 将多个装饰器应用于一个函数
由于装饰器decorators被用作功能块functionality block,有时我们希望将许多装饰器decorators集合到一个函数中。如何实现呢?
非常简单,只需将所有需要的装饰器放在函数的顶部,如下所示:
python
def add_author(func):
def wrapper():
author = 'Zhang San'
return author + '\n' + func()
return wrapper
def add_publication(func):
def wrapper():
pub = 'TechToFreedom'
return pub + '\n' + func()
return wrapper
@add_publication
@add_author
def get_title():
return '7 Levels of Using Decorators in Python'
print(get_title())
# TechToFreedom
# Zhang San
# 7 Levels of Using Decorators in Python
我们应该注意的一个重要问题是所用装饰器的顺序。如果我们改变上述示例的顺序,结果就会不同:
python
# Change the order of decorators
@add_author
@add_publication
def get_title():
return '7 Levels of Using Decorators in Python'
print(get_title())
# Zhang San
# TechToFreedom
# 7 Levels of Using Decorators in Python
事实上,多个装饰器会从下到上逐层包裹函数。上面的代码与下面的代码相同:
python
def get_title():
return '7 Levels of Using Decorators in Python'
get_title = add_author(add_publication(get_title))
print(get_title())
# Zhang San
# TechToFreedom
# 7 Levels of Using Decorators in Python
Level 3: 包装接收参数的函数
我们之前的示例程序很好,但不够灵活。如果我们在 get_title()
函数中添加一个参数,让它接收一个字符串作为标题,效果会更好。
python
def get_title(title):
return title
但如何修改装饰器以适应这种变化呢?
我们可以让包装函数wrapper function帮助我们接收参数:
python
def add_author(func):
def wrapper(title):
author = 'Zhang San'
return author + '\n' + func(title)
return wrapper
@add_author
def get_title(title):
return title
print(get_title('Harry Potter'))
# Zhang San
# Harry Potter
上面的代码已经解决了这个问题,但并不是很强大。
如前所述,装饰器是一个构件building block,可以在需要时添加到其他函数中。但是,我们无法确保所有装配了 add_author
装饰器的函数都只有一个参数。
因此,我们的装饰器decorator是有限的,不能用于包含许多参数的函数。
我们是否需要编写许多类似的装饰器decorators,只是在包装器中使用不同的参数?
幸运的是,我们不必这样做。星号技巧asterisks technique可以让我们的生活更轻松:
python
def add_author(func):
def wrapper(*args, **kwargs):
author = 'Zhang San'
return author + '\n' + func(*args, **kwargs)
return wrapper
@add_author
def get_title(title):
return title
print(get_title('Harry Potter'))
# Zhang San
# Harry Potter
@add_author
def get_many_title(t1, t2):
return t1+'\n'+t2
print(get_many_title('Harry Potter 1','Harry Potter 2'))
# Zhang San
# Harry Potter 1
# Harry Potter 2
如上所示,在星号asterisks的帮助下,我们的装饰器decorator可以装配到函数中,而无需考虑函数会收到多少个参数。
这种设计装饰器的方法既受欢迎又值得推荐,因为它能使装饰器更加灵活和强大。
Level 4: 编写可接收参数的装饰器
事实上,上一层的例子还有一个明显的错误:Zhang San is not the author of "Harry Potter"!
我们应该让我们的装饰器更加灵活,这样它就能接收到一个代表 "哈利-波特 "这一真正作者姓名的参数。
现在,事情变得有点复杂:目标函数target functions和装饰器decorator本身都应该接收参数arguments。实现这一任务的想法是在现有装饰器之外添加另一层layer。
python
def add_author_with_name(author):
def add_author(func):
def wrapper(*args, **kwargs):
return author + '\n' + func(*args, **kwargs)
return wrapper
return add_author
@add_author_with_name('J. K. Rowling')
def get_title(title):
return title
print(get_title('Harry Potter'))
# J. K. Rowling
# Harry Potter
如上所述,我们只需在 add_author
装饰器decorator中添加一个外层outer layer 来接收参数argument。
上述程序与以下程序相同:
python
def add_author_with_name(author):
def add_author(func):
def wrapper(*args, **kwargs):
return author + '\n' + func(*args, **kwargs)
return wrapper
return add_author
def get_title(title):
return title
get_title = add_author_with_name('J. K. Rowling')(get_title)
print(get_title('Harry Potter'))
# J. K. Rowling
# Harry Potter
Level 5: 保留原始函数Original Functions的元数据Metadata
到目前为止,我们已经设计出了一个非常灵活和强大的装饰器!但真正的高级工程师senior engineer会考虑到所有细节。实际上,装饰器函数decorator function还有一个隐藏的副作用。让我们通过下面的例子来了解一下:
python
def add_author(func):
def wrapper(*args, **kwargs):
author = 'Zhang San'
return author + '\n' + func(*args, **kwargs)
return wrapper
@add_author
def get_title(title):
"""
A func that receives and returns a title.
"""
return title
print(get_title.__name__)
# wrapper
print(get_title.__doc__)
# None
上述结果与预期不符。 get_title
函数的名称name和 doc 也被包装了!这是装饰器decorators的副作用。
为了避免这种副作用,我们可以手动编写一些类似 wrapper.__name__ = get_title.__name__
的代码。但还有一个更简单的方法:
python
from functools import wraps
def add_author(func):
@wraps(func)
def wrapper(*args, **kwargs):
author = 'Yang Zhou'
return author + '\n' + func(*args, **kwargs)
return wrapper
@add_author
def get_title(title):
"""
A function that receives and returns a title string.
"""
return title
print(get_title.__name__)
# get_title
print(get_title.__doc__)
# A function that receives and returns a title string.
如上所示,我们可以在 functools
模块中使用 wraps
装饰器,这将有助于我们保护元数据。在我看来,在每个封装函数中添加 wraps
装饰器是一种很好的做法,可以避免出现意想不到的结果。
Level 6: 保持简单 --- 装饰器的设计哲学
如果您达到了这一水平,那么您已经理解了,至少知道了 Python 装饰器decorators的所有核心技术core techniques。
最后但并非最不重要的一点是,在开始为项目设计装饰公司之前,有一个理念值得一提:
Keep it simple and stupid. --- A design principle noted by the U.S. Navy
保持简单和愚蠢 --- 美国海军指出的设计原则
装饰器decorator是一个优雅的工具elegant tool,可以帮助我们编写干净整洁的 Python 代码。但不要过度使用它或编写过于复杂的装饰器decorator。在我看来,一个具有三层函数three layers of functions的装饰器就足够了,将三个装饰器组装成一个函数也足够了。
俗话说As an old saying goes,过犹不及beyond is as wrong as falling short。我们应始终注意代码的可读性readability,即使使用的是复杂的功能complex feature,也要一切从简。