Understand Python Decorators

Standard Python skills are sometimes forgotten in today’s rush to learn pandas, matplotlib, numpy, scipy etc. Of course they are brilliant to learn and very rewarding, but it’s also nice to get back to basics!

Decorators are those funny @function written above the function you are calling. What do they mean?

At its simplest, a decorator is just a function that takes just one argument. That argument is the function being decorated.

Sounds complicated? It can be, but understanding what decorators do and how they work is not.

I’m going to use a simple example of a function which reverses the arguments given to it and simply prints them out to the screen.

func(1, 2 ,3) will print >> 3 2 1.

The function which reverses the arguments is cool but involves a bit of python trickery. We have to use the ‘*args’ method of passing arguments.

‘*args’ can pack arguments and can also unpack them. Let’s look at the example:

def foo(*args):
   a = args[::-1]
   print(f'original arguments {args}')
   print(f'reversed arguments {a}')

If we call foo(1,2,3), we get the following output

  • original arguments (1, 2, 3)
  • reversed arguments (3, 2, 1)

Although foo is called with (1,2,3), the foo function uses ‘*args’ which unpacks all the arguments. This means we can pass as many or as few as we like.

If we call foo(1,2,3,4,5), we get the following output

  • original arguments (1, 2, 3, 4, 5)
  • reversed arguments (5, 4, 3, 2, 1)

args[: : -1] is a simple line which is worth remembering. It uses list slicing to reverse a list!! Use [: : -1] whenever you want to reverse a list or string.

I’ll use that function to expand on decorators. It’s important to note that functions are first class objects in python. They can be treated in the same way as any other object and can be used as input to other functions.

def foo(func):
    def wrapper(*args, **kwargs):
        print(f'Calling function {func.__name__}')
        a = args[::-1]
        return func(*a)
    return wrapper
  • Line 1 defines our function, foo. It takes a function as it’s argument.
  • Line 2 defines another function called wrapper. This name is such by convention and you will see it often in decorators. It unpacks all the arguments given to func by using the *args convention. You can safely ignore **kwargs for now.
  • Line 3 simply prints “Calling function ‘func’
  • line 4 reverses our arguments
  • line 5 calls func with the packed reversed arguments *a and returns the output.
  • Line 6 returns the wrapper.

This is the basic form of a decorator function. The wrapper function does some work and calls the calling function.

This can be used as a decorator function by typing @foo above the function which will be called from your code.

@foo                
def reverse_args(*args):
    print(args)

The function reverse_args has now been decorated by the foo function! So how does it work?

  • reverse_args(1,2,3,4) will print 4 3 2 1.
  • reverse_args(‘day’, ‘the’, ‘is’, ‘This’) will print (‘This’ ‘is’ ‘the’ ‘day’)

This is about as simple as it gets to help understanding decorator functions. Just remember:

  • A decorator function is still just a plain old function.
  • The decorator function is what comes after the @ symbol.
  • The decorated function is what you finally use in your code.
  • Remember to use *args. It makes things easier.
  • The wrapper function is the function which does some work before calling the passed function. It’s called wrapper by convention to help understanding.

If you like it – share it!

Leave a comment