Execute Program

Python for Programmers: Function Decorators

Welcome to the Function Decorators lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • In an earlier lesson, we wrote function wrappers that "wrap" existing functions to give them additional functionality. For example, we can use convert_arg_to_int to wrap add1. Then add1 will work with any argument that can be converted into an integer via int(...).

  • >
    def convert_arg_to_int(func):
    def wrapped(x):
    x_as_int = int(x)
    return func(x_as_int)

    return wrapped

    def add1(x):
    return x + 1

    add1 = convert_arg_to_int(add1)
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    add1("10")
    Result:
    11Pass Icon
  • In that example, we replaced the original add1 function via add1 = convert_arg_to_int(add1). That works because functions are first-class values in Python, so we can assign them to variables.

  • However, reassigning functions in that way is quite awkward. The reassignment comes after the function definition, so it's easy to miss it. Fortunately, we can use Python's "function decorator" syntax.

  • Decorators are functions like convert_arg_to_int: they expect a function as an argument and return a new function. To use the convert_arg_to_int function as a decorator, we add @convert_arg_to_int on the line before a function declaration.

  • The @convert_arg_to_int syntax calls convert_arg_to_int on the function below it, then assigns the returned function to the original function's name. It has exactly the same effect as add1 = convert_arg_to_int(add1).

  • >
    def convert_arg_to_int(func):
    def wrapped(x):
    x_as_int = int(x)
    return func(x_as_int)

    return wrapped

    @convert_arg_to_int
    def add1(x):
    return x + 1

    add1("3")
    Result:
    4Pass Icon
  • While it's easy to miss add1 = convert_arg_to_int(add1), it's hard to imagine anyone missing the @convert_arg_to_int line. The @ stands out visually, clearly saying "there's more to this function than what you see in its body!"

  • Sometimes we want a decorator to work with any function, regardless of its arguments. To achieve that, decorators often take *args, **kwargs. Then they pass those same *args, **kwargs into the wrapped function.

  • Here's an example: silence_exceptions(func) calls func(*args, **kwargs), which passes both its arguments and keyword arguments along. If func returns a value, the decorated function returns that value too. But if func raises an exception, the decorated function returns None instead, silencing the exception.

  • >
    def silence_exceptions(func):
    def wrapped(*args, **kwargs):
    try:
    return func(*args, **kwargs)
    except Exception:
    return None

    return wrapped
  • This silence_exceptions function is very reusable! It can wrap any function, no matter what arguments it takes.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    @silence_exceptions
    def divide(a, b):
    return a / b

    divide(4, 2)
    Result:
    2.0Pass Icon
  • In the next example, we try to divide 3 by 0, which raises an exception. But our silence_exceptions decorator catches the exception and returns None.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    @silence_exceptions
    def divide(a, b):
    return a / b

    divide(3, 0)
    Result:
    NonePass Icon
  • In this case, our silence_exceptions decorator is simple. But decorators can save us a lot of duplicated code by centralizing complex behavior like caching or error handling. Error handling is especially high-stakes code, where a mistake can cause huge problems in a production system. We can reduce risk by writing error handling logic once as a decorator, testing that decorator heavily, and then applying it in many places.

  • The tradeoff is that defining decorator functions like silence_exceptions can be a bit awkward at first. Python's @some_decorator syntax doesn't affect the function body itself, so the body of silence_exceptions will be awkward no matter what. Still, the @some_decorator syntax looks nicer than a separate some_function = some_decorator(some_function) line after the function is already defined.

  • We recommend trying decorators out in many situations, but keeping the awkwardness tradeoff in mind. If a decorator is only used once, it may not be worth defining it as a separate decorator. The code may be cleaner if we move the decorator's functionality directly into the function that it decorates.

  • Finally, a note on terminology. When we apply a function, that function is "decorated". For example, the divide function in the examples above is decorated.

  • Functions like silence_exceptions are the decorators. Even if we never call silence_exceptions, it's still a decorator because it takes a function, then returns a new function that extends the original's behavior. The term "decorator" can also refer to Python's special @some_decorator syntax.