Python Decorators Explained



If you’ve spent even a little bit of time using common Python frameworks, such as Flask, you’ve undoubtedly seen decorators in use. These little tags are placed on top of a function declaration to indicate that it’s used for something special. This simple example from the Flask web site shows off a typical use case:

app = Flask(__name__)
@app.route('/')
def hello():
    name = request.args.get("name", "World")
    return f'Hello, {escape(name)}!'

The decorator is indicating that the hello() function should be the event handler the framework calls for the specified URL route. Similarly, the Errbot framework for chat bots uses decorators to mark functions that should be treated as commands in the chat. e.g. the following code produces a !hello command that causes the bot to send a response.


class HelloWorld(BotPlugin):
    """Example 'Hello, world!' plugin for Errbot."""    
    @botcmd
    def hello(self, msg, args):
        """Say hello to the world."""
        return "Hello, world!"

How does this work under the hood? Such a concise and apparently magical feature would be very useful for writing your own frameworks, or enabling plugin APIs in your application.


The Basic Principle

Decorators are a sort of syntactic sugar for something very simple: when you place a decorator in front of a function, you’re actually saying you want to pass that function to another function. So the Flask example is equivalent to something like app.route(hello), passing the hello() function to the app object’s route() method.


This seems a little less weird when you realize that functions in Python are “first-class,” meaning they can be treated as if they were any other variable. This means they can be assigned to another variable, kept in a dictionary, or passed as an argument to another function.


This is a very useful property of the language, for many reasons. In addition to enabling decorators, it’s great for mapping keys to functions, much like a more dynamic version of switch/case blocks (which Python doesn’t have).


def bananaFunc():
    print("Banana function")

def strawberryFunc():
    print("Strawberry function")

funcmap = {
    "banana": bananaFunc,
    "strawberry": strawberryFunc
}

print("Type 'banana' or 'strawberry' to continue")
x = input()
if x in funcmap.keys():
    funcmap[x]()
else:
    print("Invalid input.")

With that in mind, you can start to see how decorators work behind the scenes. So how do you make one?

There are two primary uses of a decorator:

  1. “Wrapping” a function, so extra functionality executes whenever it is invoked in the future.

  2. “Tagging” functions that you want to use later, for things like event handlers. This is more along the lines of what Flask and Errbot are doing.


The most simple structure looks like this:


def decorate(func):
    print("Do something before func()")
    func()
    return func #return the original function, unmodified
@decorate
def hello():
    print("Hello, world!")

As you can see, the decorator syntax is just a fancy way of calling a function. Nothing more. It passes the decorated function to the decorator, and expects that a function be returned. If you run the above example—note that hello() is never invoked in the usual way in this snippet—you’ll see the following output:

Do something before func()
Hello, world!

Remember: the decorator syntax is equivalent to calling decorate(hello), so the decorate() function is invoked, and it in turn calls func(). This is the behavior we will leverage to build a basic publish/subscribe system later.


But what about wrapping functions? Maybe you don’t want func() to run automatically. Maybe you want to wrap some reusable functionality around functions by simply marking them with a decorator, such as adding an authentication check that runs before the function in question.


def wrap(func):
    def wrapper():
        print("Say hello? Y/N")
        if input() == "Y":
            func()
        else:
            print("OK.")
    return wrapper
@wrap
def hello():
    print("Hello, world!")hello()

In the above code, we define a function inside the decorator and return that instead of the original function. This means the definition ofhello() is effectively replaced by wrapper(), with a reference to the original contents of hello() inside. The function also is not run, since the decorator is only defining the function, not calling it. But when we later call hello(), the wrapping becomes apparent.


The output would look something like:

Say hello? Y/N
Y
Hello, world!


A Full Example

Let’s make a quick application that demonstrates the publish/subscribe pattern. It will maintain a list of subscribers (event listeners), provide a decorator to register a function as a subscriber, and then a function to publish events to the subscribers. This example application will simply download the latest XKCD comic and publish the title and image URL.


First, let’s define the main application class, which largely consists of the publish/subscribe logic.



import urllib.request, json

class XKCDThing(object):
    def __init__(self):
        self.subscribers = [] #list of functions to call    
    def subscriber(self, func):
        self.subscribers.append(func) #decorator adds func() to list
        return func    
    def publish(self, title, link):
        for func in self.subscribers:
            func(title, link) #call every function in the list    
    def run(self):
        #do things and publish events


The subscriber() method is out decorator, taking a function and adding it to the list of subscribers before returning the function argument unmodified. This means any function we mark with the decorator will have a reference to it added to the subscribers list.


When we want to publish data to all listening functions, the publish() method loops through the list and invokes each function in turn, passing the data as arguments.


Now we can define a function to actually do something with our little event framework.

def run(self):
    url = "https://xkcd.com/info.0.json"
    with urllib.request.urlopen(url) as req:
        data = json.loads(req.read().decode())
        title = data['safe_title']
        link = data['img']
        self.publish(title, link)

This just makes an HTTP request to the XKCD API to obtain info about the newest comic, decoding the JSON and extracting the title and image URL. The interesting part is the call to the publish() method. This kicks off the calls to the decorated functions.


Now let’s make use of this by registering some listeners.


app = XKCDThing()
@app.subscriber
def eventHandler1(title, link):
    print("Event handler 1 called!")
    print(f"Title: {title}")

@app.subscriber
def eventHandler2(title, link):
    print("Event handler 2 called!")
    print(f"Link: {link}")
app.run()

Here we can see the decorators are calling methods of the app object, exactly like the Flask example from earlier. When the program runs, its broadcast message will go out to both of these functions, which can then do whatever they want.


This is a common pattern used by frameworks, or applications that want to add a plugin API with an event-based design. If you like the design philosophy behind Flask, this should help you apply it to your own projects.



Source: Medium


The Tech Platform

www.thetechplatform.com

0 comments