Python · functions

Decorators Advanced

Decorators that take arguments, and the practical ones you will write: timing, caching, access checks.

Decorator with parameters


          # decorator factory
          def repeat(times):
              def decorator(func):
                  def wrapper(*args):
                      for _ in range(times):
                          result = func(*args)
                      return result
                  return wrapper
              return decorator
          @repeat(times=3)
          def greet():
              print("Hello")
          greet()
          # ans: Hello (3 times)
        

Three functions deep

repeat(times=3)called first, with the argument
→
decoratorthe real decorator it returns
→
decorator(greet)applied to the function
→
wrappernow stored as greet

@repeat(times=3) first calls repeat(3); whatever it returns is used as the decorator.

Timing decorator


        import time
        # measure execution time
        def timer(func):
            def wrapper(*args, **kwargs):
                start = time.time()
                result = func(*args, **kwargs)
                end = time.time()
                print(f"{func.__name__}: {end-start}s")
                return result
            return wrapper
        @timer
        def slow():
            time.sleep(1)
      

Calling slow() prints about slow: 1.0s: the wrapper reads the clock before and after the real call.

Caching decorator


        from functools import wraps
        # simple cache
        def cache(func):
            cached = {}
            @wraps(func)
            def wrapper(*args):
                if args in cached:
                    return cached[args]
                result = func(*args)
                cached[args] = result
                return result
            return wrapper
        @cache
        def fibonacci(n):
            if n < 2:
                return n
            return fibonacci(n-1) + fibonacci(n-2)
      

The cache at work: fibonacci(5)

cached

Green calls are answered from cached without recursing. Without the cache, fibonacci(5) makes 15 calls.

Authentication decorator


        # check permission
        def require_auth(func):
            def wrapper(*args, **kwargs):
                if not is_authenticated():
                    raise PermissionError()
                return func(*args, **kwargs)
            return wrapper
        @require_auth
        def sensitive_operation():
            return "Secret"
      

The wrapper is a guard: if the check fails, the original function never runs.

Exercises

Predict the answer, then reveal it.

Exercise 1


          # decorator with args?
          def repeat(n):
              def decorator(func):
                  def wrapper():
                      for _ in range(n):
                          func()
                  return wrapper
              return decorator
          @repeat(2)
          def func():
              print("Hi")
          func()
          # ans: Hi (twice)
        

Exercise 2


        # lru_cache?
        from functools import lru_cache
        @lru_cache(maxsize=128)
        def fib(n):
            if n < 2:
                return n
            return fib(n-1) + fib(n-2)
        # ans: built-in caching
      

Exercise 3


        # multiple decorators with args?
        @decorator1(arg1)
        @decorator2(arg2)
        def func():
            pass
        # ans: both decorators get args
      

Exercise 4


        # decorator counting calls?
        def count_calls(func):
            def wrapper(*args):
                wrapper.calls += 1
                return func(*args)
            wrapper.calls = 0
            return wrapper
        @count_calls
        def func():
            pass
        func()
        func.calls
        # ans: 1
      

Exercise 5


        # retry decorator?
        def retry(max_attempts):
            def decorator(func):
                def wrapper(*args):
                    for attempt in range(max_attempts):
                        try:
                            return func(*args)
                        except:
                            if attempt == max_attempts-1:
                                raise
                return wrapper
            return decorator
      

Each failure is swallowed and retried; on the last attempt the bare raise lets the error through.

Exercise 6


        # validation decorator?
        def validate(type_):
            def decorator(func):
                def wrapper(arg):
                    if not isinstance(arg, type_):
                        raise TypeError()
                    return func(arg)
                return wrapper
            return decorator
        @validate(int)
        def double(x):
            return x * 2
      

double(4) returns 8; double("4") raises TypeError before double runs.

Exercise 7


        # property as decorator?
        class MyClass:
            @property
            def value(self):
                return 5
        # ans: property is a decorator
      

Exercise 8


        # classmethod as decorator?
        class MyClass:
            @classmethod
            def method(cls):
                pass
        # ans: classmethod is a decorator
      

Exercise 9


        # staticmethod as decorator?
        class MyClass:
            @staticmethod
            def method():
                pass
        # ans: staticmethod is a decorator
      

Exercise 10


        # decorator preserving signature?
        from functools import wraps
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper
        # ans: preserves func signature
      

Remember

Arguments need a factory

@d(x) calls d(x) first; it must return the actual decorator.

Wrappers can keep state

A dict in the enclosing scope (a cache) or an attribute on the wrapper (a call count) lives between calls.

Wrappers can say no

Raise before calling the original to guard it; catch and retry to make it robust.

Built-ins

lru_cache, property, classmethod, staticmethod and wraps are all decorators.