Decorators in Python Tutorial




Welcome to part 18 of the intermediate Python programming tutorial series. In this tutorial, we are going to be discussing decorators.

Decorators are a way for us to "wrap" a function inside another function, without actually hard-coding it to be like this every time. An example of this is Flask. When you go to create a new page, you simply define a new function, dictate what is returned, and then you use a decorator to give it an actual path, like:

@app.route('/contact/')
def contact():
    return render_template("contact.html")

All we had to do was describe what the page will do, then, to actually allow a browser to access it via a URL, and to handle actually returning data and all of that, we just use a decorator that comes with all of these things already.

We can use decorators on functions or methods as we see fit. Let's consider a simple example. Let's say we're writing a function that returns a brand new GPU.

def new_gpu():
    return 'a new Tesla P100 GPU!'

If we do a print(new_gpu()), we just get a new Tesla P100 GPU!

Right now, the function returns our new GPU, but it's actually a gift, so let's wrap it. To do this, we need to define our wrapping decoration:

def add_wrapping(item):
    def wrapped_item():
        return 'a wrapped up box of {}'.format(str(item()))
    return wrapped_item

The add_wrapping is going to be our decorator function, which takes a single parameter, which is item. Whatever you want to actually wrap will go in here. When we wrap something, this just simply happens. Thus, when we "decorate" the new_gpu function, that new_gpu function will be the item. Then, we have a new embedded function here that actually wraps the item (wrapped_item). This function returns the wrapped version of the return of the item that we passed. Then, back in the add_wrapping function, we just return the wrapped_item. Now, to wrap something, we can do: @add_wrapping above it, like so:

@add_wrapping
def new_gpu():
    return 'a new Tesla P100 GPU!'

All together now:

def add_wrapping(item):
    def wrapped_item():
        return 'a wrapped up box of {}'.format(str(item()))
    return wrapped_item

@add_wrapping
def new_gpu():
    return 'a new Tesla P100 GPU!'

print(new_gpu())

Output:

a wrapped up box of a new Tesla P100 GPU!

Got a bicycle?!

def new_bicycle():
    return 'a new bicycle'

We can easily wrap that too:

@add_wrapping
def new_bicycle():
    return 'a new bicycle'

print(new_bicycle())

Output:

a wrapped up box of a new bicycle

What if we...

@add_wrapping
@add_wrapping
def new_gpu():
    return 'a new Tesla P100 GPU!'

You can chain decorators like this, and what you get is what you'd expect:

a wrapped up box of a wrapped up box of a new Tesla P100 GPU!

A few notes to make here, however. When we wrap a function, we're essentially overwriting it with new information, and we're losing some information. For example, with our gpu, we could reference its __name__:

print(new_gpu.__name__)

Which gives us: wrapped_item

That may be what we wanted, or it might actually not. We may actually want to keep that original information instead. We can do:

from functools import wraps

Then use @wraps over the wrapped item:

def add_wrapping(item):
    @wraps(item)
    def wrapped_item():
        return 'a wrapped up box of {}'.format(str(item()))
    return wrapped_item

Then, if we do: print(new_gpu.__name__), we get new_gpu.

What if we want to pass arguments, like we do with Flask or Django?

We can further nest the decorator:

def add_wrapping_with_style(style):
    def add_wrapping(item):
        @wraps(item)
        def wrapped_item():
            return 'a {} wrapped up box of {}'.format(style,str(item()))
        return wrapped_item
    return add_wrapping

Notice that we've added a new parent function called add_wrapping_with_style, containing everything else from before, and with the parameter of style. Now, we can reference the style parameter within the wrapped_item function. Finally, from our new parent class, add_wrapping_with_style, we can return add_wrapping. Now, we can do something like:

@add_wrapping_with_style('beautifully')
def new_gpu():
    return 'a new Tesla P100 GPU!'

print(new_gpu())

Output:

a beautifully wrapped up box of a new Tesla P100 GPU!

Now chaining can start to have some value:

@add_wrapping_with_style('horribly')
@add_wrapping_with_style('beautifully')
def new_gpu():
    return 'a new Tesla P100 GPU!'

print(new_gpu())
a horribly wrapped up box of a beautifully wrapped up box of a new Tesla P100 GPU!

The next tutorial:





  • Intermediate Python Programming introduction
  • String Concatenation and Formatting Intermediate Python Tutorial part 2
  • Argparse for CLI Intermediate Python Tutorial part 3
  • List comprehension and generator expressions Intermediate Python Tutorial part 4
  • More on list comprehension and generators Intermediate Python Tutorial part 5
  • Timeit Module Intermediate Python Tutorial part 6
  • Enumerate Intermediate Python Tutorial part 7
  • Python's Zip function
  • More on Generators with Python
  • Multiprocessing with Python intro
  • Getting Values from Multiprocessing Processes
  • Multiprocessing Spider Example
  • Introduction to Object Oriented Programming
  • Creating Environment for our Object with PyGame
  • Many Blobs - Object Oriented Programming
  • Blob Class and Modularity - Object Oriented Programming
  • Inheritance - Object Oriented Programming
  • Decorators in Python Tutorial
  • Operator Overloading Python Tutorial
  • Detecting Collisions in our Game Python Tutorial
  • Special Methods, OOP, and Iteration Python Tutorial
  • Logging Python Tutorial
  • Headless Error Handling Python Tutorial
  • __str__ and __repr_ in Python 3
  • Args and Kwargs
  • Asyncio Basics - Asynchronous programming with coroutines