Home  >  Article  >  Backend Development  >  In-depth understanding of the usage of decorators in Python

In-depth understanding of the usage of decorators in Python

高洛峰
高洛峰Original
2017-01-23 14:46:161029browse

Because functions or classes are objects, they can also be passed around. They are mutable objects and can be changed. The behavior of changing a function or class object after it is created but before it is bound to a name is a decorator.

There are two meanings hidden after "decorator" - one is that the function plays a decorative role, for example, performs real work, and the other is an expression attached to the decorator syntax, for example, the at symbol and The name of the decorated function.

Functions can be decorated via the function decorator syntax:

@decorator       # ②
def function():    # ①
  pass

Functions are defined in a standard way. ①
Use @ as the expression defined as the prefix of the decorator function②. The part after @ must be a simple expression, usually just the name of a function or class. This part is evaluated first, and after the function defined below is ready, the decorator is called with the newly defined function object as a single parameter. The value returned by the decorator is attached to the decorated function name.
Decorators can be applied to functions and classes. The semantics for classes are clear - the class definition is used as a parameter to call the decorator, and whatever is returned is assigned to the decorated name.

Before the implementation of decorator syntax (PEP 318), the same thing could be accomplished by assigning function and class objects to temporary variables and then explicitly calling the decorator and assigning the return value to the function name. This seems to require more typing, and it is true that the decorator function name is used twice and the temporary variable is used at least three times, which is easy to make mistakes. The above example is equivalent to:

def function():         # ①
  pass
function = decorator(function)  # ②

Decorators can be stacked - the order of application is from bottom to top or from inside to outside. That is to say, the original function is used as the parameter of the first parameterizer, and whatever is returned is used as the parameter of the second decorator... Whatever the last decorator returns is attached to the name of the original function.

The decorator syntax was chosen for its readability. Because the decorator is specified before the function header and is obviously not part of the function body, it can only work on the entire function. Prefixing the expression with @ makes it too obvious to ignore (screaming in your face according to PEP... :)). When multiple decorators are applied, placing each one on a different line makes it easier to read.

Replace and adjust the original object
Decorators can either return the same function or class object or return a completely different object. In the first case, the decorator takes advantage of the fact that a function or class object is mutable to add properties, such as adding a docstring to the class. The decorator can even do useful things without changing the object, such as global Register the decorated class in the registry. In the second case, nothing is impossible: when something different replaces the decorated class or function, the new object can be completely different. However, this is not the purpose of decorators: they are meant to mutate the decorated object rather than do unexpected things. So when a function is completely replaced by a different function during decoration, the new function usually calls the original function after some preparation. Likewise, when a class is decorated into a new class, the new class usually originates from the decorated class. When the purpose of a decorator is to do something "every time", like logging every call to the decorated function, only the second type of decorator is available. On the other hand, if the first category is sufficient, it is better to use it because it is simpler.

Implementing class and function decorators
The only requirement for the decorator is that it can be called with a single parameter. This means that the decorator can be implemented as a regular function or a class with a __call__ method, and in theory, even a lambda function.

Let's compare functions and class methods. The decorator expression (the part after @) can be just the name. The name-only approach is nice (less typing, looks neat, etc.), but is only possible if there is no need to customize the decorator with parameters. The decorator of the written function can be used in the following two ways:

>>> def simple_decorator(function):
...  print "doing decoration"
...  return function
>>> @simple_decorator
... def function():
...  print "inside function"
doing decoration
>>> function()
inside function
 
>>> def decorator_with_arguments(arg):
...  print "defining the decorator"
...  def _decorator(function):
...    # in this inner function, arg is available too
...    print "doing decoration,", arg
...    return function
...  return _decorator
>>> @decorator_with_arguments("abc")
... def function():
...  print "inside function"
defining the decorator
doing decoration, abc
>>> function()
inside function

These two decorators belong to the category of returning the decorated function. If they want to return new functions, additional nesting is required, in the worst case, three levels of nesting.

>>> def replacing_decorator_with_args(arg):
...  print "defining the decorator"
...  def _decorator(function):
...    # in this inner function, arg is available too
...    print "doing decoration,", arg
...    def _wrapper(*args, **kwargs):
...      print "inside wrapper,", args, kwargs
...      return function(*args, **kwargs)
...    return _wrapper
...  return _decorator
>>> @replacing_decorator_with_args("abc")
... def function(*args, **kwargs):
...   print "inside function,", args, kwargs
...   return 14
defining the decorator
doing decoration, abc
>>> function(11, 12)
inside wrapper, (11, 12) {}
inside function, (11, 12) {}
14

_wrapper function is defined to accept all positional and keyword arguments. Usually we don't know which parameters the decorated function will accept, so the wrapper creates everything to the decorated function. An unfortunate consequence is that explicit parameters are confusing.

Complex decorators defined as classes are simpler than decorators defined as functions. When an object is created, the __init__ method is only allowed to return None, and the type of the created object cannot be changed. This means that when the decorator is defined as a class, there is no point in using the parameterless form: the final decorated object is just an instance of the decorated class, which is returned by the constructor call, which is not very useful. Discussing class-based decorators where arguments are given in decorator expressions, the __init__ method is used to construct the decorator.

>>> class decorator_class(object):
...  def __init__(self, arg):
...    # this method is called in the decorator expression
...    print "in decorator init,", arg
...    self.arg = arg
...  def __call__(self, function):
...    # this method is called to do the job
...    print "in decorator call,", self.arg
...    return function
>>> deco_instance = decorator_class('foo')
in decorator init, foo
>>> @deco_instance
... def function(*args, **kwargs):
...  print "in function,", args, kwargs
in decorator call, foo
>>> function()
in function, () {}

Decorators written as classes behave more like functions relative to the normal rules (PEP 8), so their names start with a lowercase letter.

In fact, there is little point in creating a new class that only returns the decorated function. Objects should have state and this kind of decorator is more useful when the decorator returns a new object.

>>> class replacing_decorator_class(object):
...  def __init__(self, arg):
...    # this method is called in the decorator expression
...    print "in decorator init,", arg
...    self.arg = arg
...  def __call__(self, function):
...    # this method is called to do the job
...    print "in decorator call,", self.arg
...    self.function = function
...    return self._wrapper
...  def _wrapper(self, *args, **kwargs):
...    print "in the wrapper,", args, kwargs
...    return self.function(*args, **kwargs)
>>> deco_instance = replacing_decorator_class('foo')
in decorator init, foo
>>> @deco_instance
... def function(*args, **kwargs):
...  print "in function,", args, kwargs
in decorator call, foo
>>> function(11, 12)
in the wrapper, (11, 12) {}
in function, (11, 12) {}

A decorator like this can do anything, because it can change the decorated function object and parameters, call the decorated function or not, and finally change the return value.

复制原始函数的文档字符串和其它属性
当新函数被返回代替装饰前的函数时,不幸的是原函数的函数名,文档字符串和参数列表都丢失了。这些属性可以部分通过设置__doc__(文档字符串),__module__和__name__(函数的全称)、__annotations__(Python 3中关于参数和返回值的额外信息)移植到新函数上,这些工作可通过functools.update_wrapper自动完成。

>>> import functools
>>> def better_replacing_decorator_with_args(arg):
...  print "defining the decorator"
...  def _decorator(function):
...    print "doing decoration,", arg
...    def _wrapper(*args, **kwargs):
...      print "inside wrapper,", args, kwargs
...      return function(*args, **kwargs)
...    return functools.update_wrapper(_wrapper, function)
...  return _decorator
>>> @better_replacing_decorator_with_args("abc")
... def function():
...   "extensive documentation"
...   print "inside function"
...   return 14
defining the decorator
doing decoration, abc
>>> function             

>>> print function.__doc__
extensive documentation

一件重要的东西是从可迁移属性列表中所缺少的:参数列表。参数的默认值可以通过__defaults__、__kwdefaults__属性更改,但是不幸的是参数列表本身不能被设置为属性。这意味着help(function)将显式无用的参数列表,使使用者迷惑不已。一个解决此问题有效但是丑陋的方式是使用eval动态创建wrapper。可以使用外部external模块自动实现。它提供了对decorator装饰器的支持,该装饰器接受wrapper并将之转换成保留函数签名的装饰器。

综上,装饰器应该总是使用functools.update_wrapper或者其它方式赋值函数属性。

标准库中的示例
首先要提及的是标准库中有一些实用的装饰器,有三种装饰器:

classmethod让一个方法变成“类方法”,即它能够无需创建实例调用。当一个常规方法被调用时,解释器插入实例对象作为第一个参数self。当类方法被调用时,类本身被给做第一个参数,一般叫cls。
类方法也能通过类命名空间读取,所以它们不必污染模块命名空间。类方法可用来提供替代的构建器(constructor):

class Array(object):
  def __init__(self, data):
    self.data = data
 
  @classmethod
  def fromfile(cls, file):
    data = numpy.load(file)
    return cls(data)

这比用一大堆标记的__init__简单多了。
staticmethod应用到方法上让它们“静态”,例如,本来一个常规函数,但通过类命名空间存取。这在函数仅在类中需要时有用(它的名字应该以_为前缀),或者当我们想要用户以为方法连接到类时也有用——虽然对实现本身不必要。
property是对getter和setter问题Python风格的答案。通过property装饰的方法变成在属性存取时自动调用的getter。

>>> class A(object):
...  @property
...  def a(self):
...   "an important attribute"
...   return "a value"
>>> A.a                 

>>> A().a
'a value'

    

例如A.a是只读属性,它已经有文档了:help(A)包含从getter方法获取的属性a的文档字符串。将a定义为property使它能够直接被计算,并且产生只读的副作用,因为没有定义任何setter。
为了得到setter和getter,显然需要两个方法。从Python 2.6开始首选以下语法:

class Rectangle(object):
  def __init__(self, edge):
    self.edge = edge
 
  @property
  def area(self):
    """Computed area.
 
    Setting this updates the edge length to the proper value.
    """
    return self.edge**2
 
  @area.setter
  def area(self, area):
    self.edge = area ** 0.5

   

通过property装饰器取代带一个属性(property)对象的getter方法,以上代码起作用。这个对象反过来有三个可用于装饰器的方法getter、setter和deleter。它们的作用就是设定属性对象的getter、setter和deleter(被存储为fget、fset和fdel属性(attributes))。当创建对象时,getter可以像上例一样设定。当定义setter时,我们已经在area中有property对象,可以通过setter方法向它添加setter,一切都在创建类时完成。
之后,当类实例创建后,property对象和特殊。当解释器执行属性存取、赋值或删除时,其执行被下放给property对象的方法。
为了让一切一清二楚[^5],让我们定义一个“调试”例子:

>>> class D(object):
...  @property
...  def a(self):
...   print "getting", 1
...   return 1
...  @a.setter
...  def a(self, value):
...   print "setting", value
...  @a.deleter
...  def a(self):
...   print "deleting"
>>> D.a                 

>>> D.a.fget               

>>> D.a.fset               

>>> D.a.fdel               

>>> d = D()        # ... varies, this is not the same `a` function
>>> d.a
getting 1
1
>>> d.a = 2
setting 2
>>> del d.a
deleting
>>> d.a
getting 1
1

   

属性(property)是对装饰器语法的一点扩展。使用装饰器的一大前提——命名不重复——被违反了,但是目前没什么更好的发明。为getter,setter和deleter方法使用相同的名字还是个好的风格。
一些其它更新的例子包括:

functools.lru_cache记忆任意维持有限 参数:结果 对的缓存函数(Python
3.2)
functools.total_ordering是一个基于单个比较方法而填充丢失的比较(ordering)方法(__lt__,__gt__,__le__等等)的类装饰器。
函数的废弃
比如说我们想在第一次调用我们不希望被调用的函数时在标准错误打印一个废弃函数警告。如果我们不想更改函数,我们可用装饰器

class deprecated(object):
  """Print a deprecation warning once on first use of the function.
 
  >>> @deprecated()          # doctest: +SKIP
  ... def f():
  ...   pass
  >>> f()               # doctest: +SKIP
  f is deprecated
  """
  def __call__(self, func):
    self.func = func
    self.count = 0
    return self._wrapper
  def _wrapper(self, *args, **kwargs):
    self.count += 1
    if self.count == 1:
      print self.func.__name__, 'is deprecated'
    return self.func(*args, **kwargs)

   

也可以实现成函数:

def deprecated(func):
  """Print a deprecation warning once on first use of the function.
 
  >>> @deprecated           # doctest: +SKIP
  ... def f():
  ...   pass
  >>> f()               # doctest: +SKIP
  f is deprecated
  """
  count = [0]
  def wrapper(*args, **kwargs):
    count[0] += 1
    if count[0] == 1:
      print func.__name__, 'is deprecated'
    return func(*args, **kwargs)
  return wrapper

   

while-loop移除装饰器
例如我们有个返回列表的函数,这个列表由循环创建。如果我们不知道需要多少对象,实现这个的标准方法如下:

def find_answers():
  answers = []
  while True:
    ans = look_for_next_answer()
    if ans is None:
      break
    answers.append(ans)
  return answers

   

只要循环体很紧凑,这很好。一旦事情变得更复杂,正如真实的代码中发生的那样,这就很难读懂了。我们可以通过yield语句简化它,但之后用户不得不显式调用嗯list(find_answers())。

我们可以创建一个为我们构建列表的装饰器:

def vectorized(generator_func):
  def wrapper(*args, **kwargs):
    return list(generator_func(*args, **kwargs))
  return functools.update_wrapper(wrapper, generator_func)

   

然后函数变成这样:

@vectorized
def find_answers():
  while True:
    ans = look_for_next_answer()
    if ans is None:
      break
    yield ans

   

插件注册系统
这是一个仅仅把它放进全局注册表中而不更改类的类装饰器,它属于返回被装饰对象的装饰器。

class WordProcessor(object):
  PLUGINS = []
  def process(self, text):
    for plugin in self.PLUGINS:
      text = plugin().cleanup(text)
    return text
 
  @classmethod
  def plugin(cls, plugin):
    cls.PLUGINS.append(plugin)
 
@WordProcessor.plugin
class CleanMdashesExtension(object):
  def cleanup(self, text):
    return text.replace('—', u'\N{em dash}')

   

这里我们使用装饰器完成插件注册。我们通过一个名词调用装饰器而不是一个动词,因为我们用它来声明我们的类是WordProcessor的一个插件。plugin方法仅仅将类添加进插件列表。

关于插件自身说下:它用真正的Unicode中的破折号符号替代HTML中的破折号。它利用unicode literal notation通过它在unicode数据库中的名称(“EM DASH”)插入一个符号。如果直接插入Unicode符号,将不可能区分所插入的和源程序中的破折号。

更多深入理解Python中装饰器的用法相关文章请关注PHP中文网!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn