Python ยท functions

Decorators Basics

A decorator takes a function and hands back a new one that wraps it, adding behaviour before and after.

Basic decorator


          # simple decorator
          def my_decorator(func):
              def wrapper():
                  print("Before")
                  func()
                  print("After")
              return wrapper
          @my_decorator
          def say_hello():
              print("Hello!")
          say_hello()
          # ans: Before
          # ans: Hello!
          # ans: After
        

What the @ line does

  1. @my_decorator passes say_hello in and stores the returned wrapper under the name say_hello.
  2. Calling say_hello() now runs wrapper, which calls the original in the middle.

Decorator with arguments


          # decorator for functions with args
          def my_decorator(func):
              def wrapper(*args, **kwargs):
                  print("Before")
                  result = func(*args, **kwargs)
                  print("After")
                  return result
              return wrapper
          @my_decorator
          def add(a, b):
              return a + b
          add(5, 3)
          # ans: Before, After, returns 8
        

*args, **kwargs accept any arguments and pass them straight through; return result hands back the original's value.

Manual decoration


        # without @ syntax
        def greet():
            print("Hello")
        greet = my_decorator(greet)
        # ans: equivalent to @my_decorator
      

          @my_decorator
          def greet():
              print("Hello")
        
=

          def greet():
              print("Hello")
          greet = my_decorator(greet)
        

Preserving metadata


        from functools import wraps
        # preserve function info
        def my_decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper
        @my_decorator
        def my_func():
            """Docstring"""
            pass
        my_func.__name__
        # ans: "my_func" (not "wrapper")
      

@wraps(func) copies the original's name and docstring onto the wrapper.

Exercises

Predict the answer, then reveal it. Some slides then animate why.

Exercise 1


        # what is returned?
        def decorator(func):
            def wrapper():
                return func()
            return wrapper
        @decorator
        def func():
            return 5
        func()
        # ans: 5
      

Exercise 2


          # decorator execution?
          def decorator(func):
              print("Decorating")
              return func
          @decorator
          def func():
              pass
          # ans: "Decorating" (at definition time)
        

Exercise 3


          # nested decorators?
          def dec1(func):
              def wrapper():
                  print("1")
                  return func()
              return wrapper
          def dec2(func):
              def wrapper():
                  print("2")
                  return func()
              return wrapper
          @dec1
          @dec2
          def func():
              pass
          func()
          # ans: 1, 2
          # ans: (applied bottom up; dec1's wrapper runs first)
        

Exercise 4


        # decorator without wrapper?
        def decorator(func):
            return func
        @decorator
        def func():
            pass
        # ans: valid (identity decorator)
      

Exercise 5


        # accessing original?
        def decorator(func):
            def wrapper():
                return func()
            wrapper.original = func
            return wrapper
        @decorator
        def func():
            pass
        func.original
      

Functions are objects, so the wrapper can carry attributes: func.original is the undecorated function.

Exercise 6


          # decorator modifying return?
          def double(func):
              def wrapper(*args):
                  return func(*args) * 2
              return wrapper
          @double
          def add(a, b):
              return a + b
          add(2, 3)
          # ans: 10
        

Exercise 7


          # decorator with closure?
          def make_bold(func):
              def wrapper():
                  return f"<b>{func()}</b>"
              return wrapper
          @make_bold
          def get_text():
              return "Hello"
          get_text()
          # ans: "<b>Hello</b>"
        

Exercise 8


        # class as decorator?
        class Decorator:
            def __init__(self, func):
                self.func = func
            def __call__(self, *args):
                return self.func(*args)
        @Decorator
        def func():
            pass
      

Any callable works: func becomes a Decorator instance, and calling it runs __call__.

Exercise 9


        # without @wraps?
        def decorator(func):
            def wrapper():
                return func()
            return wrapper
        @decorator
        def func():
            pass
        func.__name__
        # ans: "wrapper"
      

Exercise 10


          # decorator returns None?
          def decorator(func):
              def wrapper():
                  func()
              return wrapper
          @decorator
          def func():
              return 5
          func()
          # ans: None (wrapper doesn't return)
        

Remember

@d means f = d(f)

It runs once, when the function is defined.

The wrapper replaces the name

Calls go to the wrapper, which decides when (and whether) to call the original.

Pass things through

Use *args, **kwargs and return the result, or the caller gets None.

Stacking

The decorator nearest the def wraps first; the top one runs first.

@wraps(func)

Keeps __name__ and the docstring of the original.