作者:Jeff Knupp
原文地址:https://jeffknupp.com/blog/2013/11/29/improve-your-python-decorators-explained/
之前我写了关于yield与generators。在那篇文章里,我提到它是新手觉得困惑的话题。修饰器(decorator)的目的与创建是另一个这样的话题(不过,使用它们相当容易)。在本文中,你将学习修饰器是什么,如何创建它们,以及为什么它们是有用的。
在我们开始前,回忆在Python中一切都是可像值一样处理的对象(比如函数、类、模块)。你可以向这些对象绑定名字,将它们作为实参传递给函数,以及从函数返回它们(连同其他)。下面的代码就是我所讲的一个例子:
def is_even(value):
"""Return True if *value* is even."""
return (value % 2) == 0
def count_occurrences(target_list, predicate):
"""Return the number of times applying the callable *predicate* to a
list element returns True."""
return sum([1 for e in target_list if predicate(e)])
my_predicate = is_even
my_list = [2, 4, 6, 7, 9, 11]
result = count_occurrences(my_list, my_predicate)
print(result)
我们编写了一个接受一个列表及另一个函数(恰好是一个断言函数,意味着基于传递给它的实参的某个属性,它返回True或False)的函数,它返回断言函数对列表中元素成立的次数。虽然有实现这的内置函数,对于展示目的,它是有用的。
魔法在行my_predicate = is_even。我们将名字my_predicate绑定到这个函数本身(不是调用它时返回的值),像任何“普通”变量那样使用它。将它传递给count_occurrences允许count_occurences将这个函数应用到列表的元素,即使它不知道my_predicate做什么。它只是假设这是一个使用单个实参调用并返回True或False的函数。
希望这对你来说是老生常谈。不过,如果这是你第一次看到以这个方式使用函数,我建议在继续之前,阅读Drastically Improve Your Python: Understanding Python's Execution Model。
我们刚看到函数可以作为实参传递给其他函数。它们还可以作为返回值从函数返回。下面展示了这可能的用处:
def surround_with(surrounding):
"""Return a function that takes a single argument and."""
def surround_with_value(word):
return '{}{}{}'.format(surrounding, word, surrounding)
return surround_with_value
def transform_words(content, targets, transform):
"""Return a string based on *content* but with each occurrence
of words in *targets* replaced with
the result of applying *transform* to it."""
result = ''
for word in content.split():
if word in targets:
result += ' {}'.format(transform(word))
else:
result += ' {}'.format(word)
return result
markdown_string = 'My name is Jeff Knupp and I like Python but I do not own a Python'
markdown_string_italicized = transform_words(markdown_string, ['Python', 'Jeff'],
surround_with('*'))
print(markdown_string_italicized)
transform_words函数的目的是搜索在content中出现的targets列表中任何单词,并对它们应用transform实参。在我们的例子里,设想我们有一个Markdown字符串,并希望斜体化所有出现的单词Python与Jeff(在由星号围绕时,一个单词在Markdown里会斜体化)。
这里我们利用了函数可以作为调用一个函数的结果返回的事实。在这个过程里,我们创建了一个新函数,在调用时,前后附着给定的实参。然后,我们将这个新函数作为一个实参传递给transform_words,在那里它应用到我们搜索列表:([‘Python’, ‘Jeff’])中的单词上。
你可以将surround_with视为一个小的函数“工厂”。它待在那里等待创建一个函数。你给它一个值,它给回你一个将使用你给它的值围绕一个单词的函数。理解这里发生了什么是理解修饰器的关键。我们的“函数工厂”甚至不返回一个“普通”值;它总是返回一个新函数。注意surround_with实际上自己不做这个围绕,它只是创建一个在需要时可以执行围绕的函数。
Surround_with_value利用嵌套函数能访问绑定在创建它们的作用域内名字的事实。因此,surround_with_value不需要任何特殊机制来访问surrounding(这将破坏我们的目标)。它只是“知道”它可以访问它,在需要时使用之。
现在我们已经看到函数可以作为实参发送给一个函数以及作为一个函数的结果。如果我们一起使用这两个事实会怎么样?我们能创建一个接受一个函数作为参数并返回一个函数作为结果的函数吗?那样会有什么用处?
确实可以。设想我们正在使用一个web框架,模型带有许多货币相关域,像price,cart_subtotal,savingdeng。理想地,当我们输出这些域时,我们将总是前加一个“$”。如果我们能以某种方式标记产生这些值的函数,使之自动这样做,那就太好了。
这正是修饰器的工作。下面的函数展示应用了tax的price:
class Product(db.Model):
name = db.StringColumn
price = db.FloatColumn
def price_with_tax(self, tax_rate_percentage):
"""Return the price with *tax_rate_percentage* applied.
*tax_rate_percentage* is the tax rate expressed as a float, like
"7.0" for a 7% tax rate."""
return price * (1 + (tax_rate_percentage * .01))
如何使用语言来扩展这个函数,使得返回值前加上一个“$”?我们创建一个修饰器函数,它有一个有用的缩写记法:@。为了创建我们的修饰符,我们创建了一个接受一个函数(要修饰的函数)并返回一个新函数(应用了修饰的原始函数)的函数。下面是在我们的应用程序中如何做到这:
def currency(f):
def wrapper(*args, **kwargs):
return '$' + str(f(*args, **kwargs))
return wrapper
我们包括了arg与kwargs作为wrapper函数的参数,使之更灵活。因为,我们不知道要封装的函数的参数(wrapper需要调用该函数),我们接受所有的位置(positional)实参(*args)以及关键字(keyword)实参(**args)作为参数,并把它们“转发”给该函数调用。
定义了currency,现在我们可以使用修饰器记法来修饰我们的price_with_tax函数,像这样:
class Product(db.Model):
name = db.StringColumn
price = db.FloatColumn
@currency
def price_with_tax(self, tax_rate_percentage):
"""Return the price with *tax_rate_percentage* applied.
*tax_rate_percentage* is the tax rate expressed as a float, like "7.0"
for a 7% tax rate."""
return price * (1 + (tax_rate_percentage * .01))
现在,对其他代码,看起来price_with_tax就是一个返回前带美元符的含税价格的函数。不过,注意,我们没有改变price_with_tax本身任何代码来实现这。我们只是使用一个修饰器“修饰”这个函数,给予它额外的功能。
简短的题外话
一个问题(容易解决)是使用currency封装price_with_tax,将其.__name__与.__doc__改变为currency的,这显然不是我们希望的。Functools模块包含了一个有用的工具,wraps,它将把这些值恢复为我们预期的值。它像这样使用:
from functools import wraps
def currency(f):
@wraps(f)
def wrapper(*args, **kwargs):
return '$' + str(f(*args, **kwargs))
return wrapper
使用附加功能封装一个函数而不改变被封装函数的这个记法是及其强大且有用的。使用修饰器可以完成许多事情,否则就需要大量的样板代码,或者根本就不可能。它们也作为框架及库提供功能的一个便利方式。Flask使用修饰器作为向web应用程序添加新终结点(endpoint)的方式,就像文档里的这个例子:
@app.route('/')
def hello_world():
return 'Hello World!'
注意修饰器(本身是函数)可以接受实参。我将把修饰器实参,连同类修饰器,留到本系列的下一篇文章。
今天我们学习了如何使用修饰器操纵这个语言(很像C宏),使用我们正在操纵的语言(即Python)。这有非常强大的含义,我们将在下一篇文章里探讨。不过,现在你应该应该牢固掌握了如何创建及使用绝大多数修饰器。更重要的,现在你应该理解它们如何工作,以及何时有用。