Python: Decorators

Wrap to alter or enhance the object being wrapped.

@decorate
def func(x):
    ...
# shorthand for
def func(x):
    ...
func = decorate(func)

Like:

def trace(func):
  def call(*args, **kwargs):
    print("Calling", func.__name__)
    return func(*args, **kwargs)
  return call
@trace
def square(x):
  '''
  >>> square(3)
  9
  '''
  return x*x
print(square(3))
print("doc string:", square.__doc__) # doc string: None

Function metadata like doc string is now hidden for the wrapped function square.

Best practice:

  • DO wrap using functools.wraps
from functools import wraps
def trace(func):
  @wraps(func)
  def call(*args, **kwargs):
    print("Calling", func.__name__)
    return func(*args, **kwargs)
  return call
@trace
def square(x):
  """
  >>> square(3)
  9
  """
  return x*x
print(square(3))
print("doc string:", square.__doc__) # doc string:   >>> square(3)
  9

@wraps() decorator from functools copies function metadata into the replacement function.

When there are multiple decorators, the order might matter:

@decorator1
@decorator2
def func(x):
    pass
# shorthand for
def func(x):
    ...
func = decorator1(decorator2(func))

For example:

class SomeClass(object):
    @classmethod # Ok
    @trace
    def a(cls): # a = classmethod(trace(a))
        pass
    @trace # Error: @classmethod returns classmethod descriptor object which @trace was not designed to handle
    @classmethod
    def b(cls): # b = trace(classmethod(b))
        pass

A decorator can accept arguments.

@trace("You have called {func.__name__}")
def func():
    pass
# shorthand for
def func():
    pass
temp = trace("You have called {func.__name__}")
func = temp(func)
# --------------------------------------
from functools import wraps
def trace(message):
    def decorate(func): # extra wrap to utilize the message arg
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(message.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorate

The temp above acts as a “decorator factory”. So, we could simplify:

logged = trace("You have called {func.__name__}")
@logged
def func1():
    pass
@logged
def func2():
    pass

Decorator may not modify the function output, may just do registration.

@eventhandler("BUTTON")
def handle_button(msg):
    ...
@eventhandler("RESET"):
def handle_reset(msg):
    ...
_event_handlers = {}
def eventhandler(event):
    def register_function(func):
        _event_handlers[event] = func
        return func
    return register_function   

Leave a comment