>  기사  >  백엔드 개발  >  Python에서 데코레이터를 사용하는 방법

Python에서 데코레이터를 사용하는 방법

高洛峰
高洛峰원래의
2017-03-01 14:08:031101검색

함수나 클래스는 객체이므로 전달할 수도 있습니다. 그것들은 변경 가능한 객체이며 변경될 수 있습니다. 함수나 클래스 객체가 생성된 후 이름에 바인딩되기 전에 이를 변경하는 동작을 데코레이터라고 합니다.

"데코레이터" 뒤에는 두 가지 의미가 숨겨져 있습니다. 하나는 함수가 장식적인 역할을 한다는 것입니다(예: 실제 작업 수행). 다른 하나는 데코레이터 구문에 첨부된 표현식입니다. , at 기호와 장식된 함수의 이름입니다.

함수는 함수 데코레이터 구문을 통해 장식할 수 있습니다.

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

함수는 표준 방식으로 정의됩니다. ①
데코레이터 함수의 접두사로 정의된 표현식으로 @를 사용합니다. @ 뒤의 부분은 간단한 표현식이어야 하며 일반적으로 함수나 클래스의 이름만 있으면 됩니다. 이 부분을 먼저 평가하고, 아래에 정의된 함수가 준비된 후 새로 정의된 함수 객체를 단일 매개변수로 하여 데코레이터를 호출합니다. 데코레이터가 반환한 값은 데코레이팅된 함수 이름에 첨부됩니다.
데코레이터는 함수와 클래스에 적용될 수 있습니다. 클래스의 의미는 명확합니다. 클래스 정의는 데코레이터를 호출하는 매개변수로 사용되며, 반환되는 모든 항목은 데코레이팅된 이름에 할당됩니다.

데코레이터 구문(PEP 318)을 구현하기 전에 함수 및 클래스 객체를 임시 변수에 할당한 다음 명시적으로 데코레이터를 호출한 다음 반환 값을 함수 이름에 할당하면 동일한 작업을 수행할 수 있습니다. 이는 더 많은 타이핑이 필요할 것으로 보이며 데코레이터 함수 이름을 두 번 사용하고 임시 변수를 세 번 이상 사용하므로 실수하기 쉬운 것이 사실입니다. 위의 예는 다음과 같습니다.

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

데코레이터는 쌓을 수 있습니다. 적용 순서는 아래에서 위로 또는 내부에서 외부입니다. 즉, 원래 함수는 첫 번째 매개변수의 매개변수로 사용되고, 반환되는 모든 것은 두 번째 데코레이터의 매개변수로 사용됩니다... 마지막 데코레이터가 반환하는 모든 것은 원래 함수의 이름에 첨부됩니다.

가독성을 위해 데코레이터 구문을 선택했습니다. 데코레이터는 함수 헤더 앞에 지정되고 분명히 함수 본문의 일부가 아니기 때문에 전체 함수에서만 작동할 수 있습니다. 표현식 앞에 @를 붙이면 무시하기에는 너무 분명해집니다(PEP에 따르면 얼굴에 비명을 지릅니다... :)). 여러 개의 데코레이터를 적용할 때 각각을 다른 줄에 배치하면 읽기가 더 쉽습니다.

원본 객체 교체 및 조정
데코레이터는 동일한 함수 또는 클래스 객체를 반환하거나 완전히 다른 객체를 반환할 수 있습니다. 첫 번째 경우 데코레이터는 클래스에 독스트링을 추가하는 등 속성을 추가하기 위해 함수나 클래스 객체가 변경 가능하다는 사실을 활용합니다. 레지스트리에 장식된 클래스가 있습니다. 두 번째 경우에는 불가능한 것이 없습니다. 장식된 클래스나 함수를 다른 것으로 대체하면 새 개체가 완전히 다를 수 있습니다. 그러나 이는 데코레이터의 목적이 아닙니다. 데코레이터는 예상치 못한 작업을 수행하기보다는 데코레이팅된 개체를 변경하기 위한 것입니다. 따라서 데코레이션 중에 함수가 다른 함수로 완전히 대체되면 일반적으로 새 함수는 약간의 준비 후에 원래 함수를 호출합니다. 마찬가지로 클래스가 새로운 클래스로 장식되면 일반적으로 새 클래스는 장식된 클래스에서 시작됩니다. 데코레이터의 목적이 데코레이팅된 함수에 대한 모든 호출을 기록하는 것과 같이 "매번" 작업을 수행하는 것인 경우 두 번째 유형의 데코레이터만 사용할 수 있습니다. 반면, 첫 번째 카테고리가 충분하다면, 더 간단하기 때문에 사용하는 것이 좋습니다.

클래스 및 함수 데코레이터 구현
데코레이터의 유일한 요구 사항은 단일 매개변수로 호출할 수 있다는 것입니다. 즉, 데코레이터는 일반 함수나 __call__ 메서드가 있는 클래스로 구현될 수 있으며 이론적으로는 람다 함수로도 구현될 수 있습니다.

함수와 클래스 메소드를 비교해 보겠습니다. 데코레이터 표현식(@ 다음 부분)은 이름일 수 있습니다. 이름만 사용하는 접근 방식은 훌륭하지만(입력 횟수가 적고 깔끔해 보이는 등) 매개 변수를 사용하여 데코레이터를 사용자 정의할 필요가 없는 경우에만 가능합니다. 작성 중인 함수의 데코레이터는 다음 두 가지 방법으로 사용할 수 있습니다.

>>> 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

이 두 데코레이터는 데코레이팅된 함수를 반환하는 범주에 속합니다. 새 함수를 반환하려면 추가 중첩이 필요하며 최악의 경우 세 가지 수준의 중첩이 필요합니다.

>>> 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 함수는 모든 위치 및 키워드 인수를 허용하도록 정의되었습니다. 일반적으로 우리는 장식된 함수가 어떤 매개변수를 받아들일지 모르기 때문에 래퍼는 장식된 함수에 대한 모든 것을 생성합니다. 불행한 결과는 명시적 매개변수가 혼란스럽다는 것입니다.

클래스로 정의된 복잡한 데코레이터는 함수로 정의된 데코레이터보다 간단합니다. 객체 생성 시 __init__ 메소드는 None 반환만 허용되며, 생성된 객체의 타입은 변경할 수 없습니다. 이는 데코레이터가 클래스로 정의되면 매개변수 없는 형식을 사용할 필요가 없다는 것을 의미합니다. 최종 데코레이팅된 객체는 생성자(생성자) 호출에 의해 반환되는 데코레이팅된 클래스의 인스턴스일 뿐입니다. 유용한. 데코레이터 표현식에 인수가 제공되는 클래스 기반 데코레이터에 대해 설명하면 __init__ 메서드가 데코레이터를 구성하는 데 사용됩니다.

>>> 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, () {}

클래스로 작성된 데코레이터는 일반 규칙(PEP 8)에 비해 함수처럼 동작하므로 이름이 소문자로 시작됩니다.

사실 데코레이션된 함수만 반환하는 새 클래스를 만드는 것은 별 의미가 없습니다. 객체에는 상태가 있어야 하며 이러한 종류의 데코레이터는 데코레이터가 새 객체를 반환할 때 더 유용합니다.

>>> 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) {}

像这样的装饰器可以做任何事,因为它能改变被装饰函数对象和参数,调用被装饰函数或不调用,最后改变返回值。

复制原始函数的文档字符串和其它属性
当新函数被返回代替装饰前的函数时,不幸的是原函数的函数名,文档字符串和参数列表都丢失了。这些属性可以部分通过设置__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              
<function function at 0x...>
>>> 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                  
<property object at 0x...>
>>> A().a
&#39;a value&#39;


例如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                  
<property object at 0x...>
>>> D.a.fget                
<function a at 0x...>
>>> D.a.fset                
<function a at 0x...>
>>> D.a.fdel                
<function a at 0x...>
>>> 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__, &#39;is deprecated&#39;
    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__, &#39;is deprecated&#39;
    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(&#39;—&#39;, u&#39;\N{em dash}&#39;)

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

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

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

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.