ProgrammingPython Backend Developer

What are decorators for functions in Python, what is their history, and why are they used in modern programming?

Pass interviews with Hintsage AI assistant

Answer

History

Decorators appeared in Python as syntactic sugar starting from version 2.4 to facilitate working with higher-order functions—those that take or return other functions. The evolution of approaches to extending the functionality of functions led to a format of concise and expressive means—annotations via @decorator.

Problem

In large projects, it is often necessary to modify or wrap functions with some additional logic: logging, access checking, caching, measuring execution time. Without decorators, it was necessary to manually call wrapper functions, which bloated the code.

Solution

Decorators allow for extracting repeated aspects into separate wrappers, thereby increasing the readability and reusability of code. Using @decorator, functionality can be elegantly added to functions and methods:

Example code:

import time def timing_decorator(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f'Elapsed: {time.time() - start:.3f}s') return result return wrapper @timing_decorator def slow_function(): time.sleep(0.5) slow_function() # Will output how long it took to execute

Key features:

  • Allow for code reuse (DRY principle);
  • Can be used for functions, methods, classes;
  • Allow for centralized implementation of cross-functional tasks (logging, caching, access, profiling);

Tricky Questions.

Can decorators change the signature of the wrapped function?

It is often erroneously believed that the signature of a function is not changed by a decorator. In fact, without using the functools.wraps module, the metadata disappears, which can lead to unexpected errors in auto-documentation or introspection.

Example code:

from functools import wraps def decorator(func): @wraps(func) def wrapper(*args): return func(*args) return wrapper

Can decorators be parameterized?

It is often answered that they cannot. In fact, it is possible to create a parameterized decorator through an additional level of nesting—a function that returns a decorator.

Example code:

def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def hello(): print("Hello!")

Can multiple decorators be applied to one function? What will be the execution order?

It is often mistakenly believed that the order is unimportant or that it matches the order of placement of decorators in code. In fact, the lowest decorator is applied first, then the next one, and so on upwards.

Example code:

def dec1(f): def wrapper(*a, **k): print("dec1") return f(*a, **k) return wrapper def dec2(f): def wrapper(*a, **k): print("dec2") return f(*a, **k) return wrapper @dec1 @dec2 def f(): print("core") f() # dec1, dec2, core

Common Mistakes and Anti-Patterns

  • Ignoring functools.wraps loses metadata about the original function;
  • The logic of the decorator does not account for exceptions; one cannot catch and handle errors inside;
  • Unobvious overlapping of multiple decorators.

Real Life Example

Negative Case

Timing logging was added to 10 functions manually, by copy-pasting code.

Pros:

  • Logic is clear, code is nearby, easy to find an error.

Cons:

  • Difficult to maintain—requires changing dozens of places if behavior needs to change.
  • Code is duplicated, violating DRY.

Positive Case

All timing logic is extracted into a decorator; all functions needing this metric are wrapped with the @timing_decorator.

Pros:

  • Changes are made centrally;
  • Code is shorter and more readable.

Cons:

  • Possible loss of signature information without functools.wraps, if not careful;
  • Newcomers find it harder to immediately understand how decorators work.