Python Decorators

I was looking into the kdtree repository this week and came up with the require_axis decorator function. I have used some of the built-in decorators in python, but function decorators seem pretty powerful at the first glance so I decided to dig it a bit.

Built-in decorators

Decorators don’t differ from ordinary functions, you only call them in a fancier way.

stackoverflow

However, some are often used in the fancier way – namely @property, @classmethod and @staticmethod.

Function Decorators

Into the world of user-defined decorators, we need to understand first the most substantial utility of this syntactic sugar: to wrap a function and modify (usually enhance) its behavior. A snippet of a simple decorator is as follows.

"""define decorator"""
def p_decorate(f):
  def wrapper(name):
    return "<p>{0}</p>".format(f(name))
  return wrapper
"""use decorator"""
@p_decorate
def get_victor(name):
  return "the victor is {0}".format(name)
# print(get_victor("Me"))
# output - <p>the victor is Me</p>

Decorator with arguments

There is room for better extensibility with the decorators. First, it’s possible to pass arguments to them.

"""define decorator"""
def tag(sign):
  def tag_decorate(f):
    def wrapper(name):
      return "<{0}>{1}</{0}>".format(sign, f(name))
    return wrapper
  return tag_decorate
"""use decorator"""
@tag("b")
def get_victor(name):
  return "the victor is {0}".format(name)
# print(get_victor("Me"))
# output - <b>the victor is Me</b>

Decorator for class methods

In a class, methods are expected to reference the current object as their first parameter. It brings confusion when designing a decorator for functions and class methods alike. The solution is to build the wrapper function so that it accepts arbitrary number of parameters.

"""define decorator"""
def p_decorate(f):
  def wrapper(*args, **kwargs):
    return "<p>{0}</p>".format(f(*args, **kwargs))
  return wrapper
"""use decorator in class methods"""
class Person(object):
  def __init__(self):
    self.name = "John"
    self.family = "Doe"
  @p_decorate
  def get_fullname(self):
    return self.name + " " + self.family
# Person().get_fullname()
# output - <p>John Doe</p>

Signature overloading

Decorators will overload the name, module and docstring of the original function:

print(get_victor.__name__)
# Outputs wrapper

We can use functools.wraps to reset these signatures to the original state.

from functools import wraps
"""define decorator"""
def tag(sign):
  def tag_decorate(f):
    @wraps(f)
    def wrapper(name):
      return "<{0}>{1}</{0}>".format(sign, f(name))
    return wrapper
  return tag_decorate
"""use decorator"""
@tag("b")
def get_victor(name):
  """return the name of the victor"""
  return "the victor is {0}".format(name)
# print(get_victor.__name__) # get_victor
# print(get_victor.__doc__) # return the name of the victor
# print(get_victor.__module__) # __main__

References