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_intto wrapadd1. Thenadd1will work with any argument that can be converted into an integer viaint(...).>
def convert_arg_to_int(func):def wrapped(x):x_as_int = int(x)return func(x_as_int)return wrappeddef add1(x):return x + 1add1 = convert_arg_to_int(add1)- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add1("10")Result:
11
In that example, we replaced the original
add1function viaadd1 = 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 theconvert_arg_to_intfunction as a decorator, we add@convert_arg_to_inton the line before a function declaration.The
@convert_arg_to_intsyntax callsconvert_arg_to_inton the function below it, then assigns the returned function to the original function's name. It has exactly the same effect asadd1 = 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 wrappedconvert_arg_to_intdef add1(x):return x + 1add1("3")Result:
4
While it's easy to miss
add1 = convert_arg_to_int(add1), it's hard to imagine anyone missing the@convert_arg_to_intline. 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, **kwargsinto the wrapped function.Here's an example:
silence_exceptions(func)callsfunc(*args, **kwargs), which passes both its arguments and keyword arguments along. Iffuncreturns a value, the decorated function returns that value too. But iffuncraises an exception, the decorated function returnsNoneinstead, silencing the exception.>
def silence_exceptions(func):def wrapped(*args, **kwargs):try:return func(*args, **kwargs)except Exception:return Nonereturn wrappedThis
silence_exceptionsfunction 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_exceptionsdef divide(a, b):return a / bdivide(4, 2)Result:
2.0
In the next example, we try to divide 3 by 0, which raises an exception. But our
silence_exceptionsdecorator catches the exception and returnsNone.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
silence_exceptionsdef divide(a, b):return a / bdivide(3, 0)Result:
None
In this case, our
silence_exceptionsdecorator 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_exceptionscan be a bit awkward at first. Python's@some_decoratorsyntax doesn't affect the function body itself, so the body ofsilence_exceptionswill be awkward no matter what. Still, the@some_decoratorsyntax looks nicer than a separatesome_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
dividefunction in the examples above is decorated.Functions like
silence_exceptionsare the decorators. Even if we never callsilence_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_decoratorsyntax.