Python in Detail: Decorators as an API Tool
Welcome to the Decorators as an API Tool lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen decorators from a few different angles. In this lesson, we'll explore some real-world use cases. Some of those cases may be surprising: decorators don't always have to return functions!
First, here's a simple, normal decorator. It decorates any function to convert all arguments into integers, no matter what they were originally.
>
def int_caster(func):def wrapped(x):x_as_int = int(x)return func(x_as_int)return wrappedint_casterdef doubler(x):return 2 * xdoubler("3")Result:
6
So far, our function decorators have returned new functions, like
@int_casterdoes above. However, decorators don't have to return functions; they can return any value.The next pair of examples shows that in action. They're a bit contrived, but we'll follow them with a real-world example.
The
map_over_datafunction below applies a functionfuncto every element in the globalintegerslist. First we'll look at it without using the decorator syntax. These are just normal Python functions.>
integers = [1, 3, 5]def map_over_data(func):return [func(el) for el in integers]def increment(x):return x + 1increment = map_over_data(increment)incrementResult:
[2, 4, 6]
The
increment = map_over_data(increment)line does exactly what decorators do. We can rewrite it with the@syntax, giving us the same result as above.>
integers = [1, 3, 5]# We use this as a decorator, but it doesn't return a function!def map_over_data(func):return [func(el) for el in integers]map_over_datadef increment(x):return x + 1incrementResult:
[2, 4, 6]
Remember: putting the
@map_over_dataline abovedef incrementmeans exactly the same thing asincrement = map_over_data(increment). There's nothing else happening when we use a decorator. Decorators only pass a function to another function, then assign the result to the original function's name.In practice, most function decorators do return new functions. But sometimes we use decorators as helpers for building other values, like class instances. This is particularly useful when designing public APIs for libraries.
Suppose that we want to write handlers for web requests. When the user loads
/in their browser, we should run a function that shows them the landing page. When they load/users/amir, we should run a different function that shows Amir's account. Each of these functions is a handler for a specific path on the site.Here's a sketch of a class that can represent any handler for any path. It takes a regular expression for each path. For example, the regex
r"^/users/\w+$"matches paths like"/users/amir"and"/users/betty". (Don't worry if the details of regexes are unfamiliar! We're concerned with the decorators here, not the regular expressions.)The route handler class also takes a function. When a user visits a URL that matches the regex, the class calls the function to handle that request.
>
class Handler:def __init__(self, path_regex, func):self.path_regex = path_regexself.func = funcdef get_response(self, request):return self.func(request)We'll call
Handler's.get_responsemethod in isolation, to make sure that it works.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
def handle_user(request):return f"<p>Hello, {request['name']}</p>"handle_user = Handler(r"^/users/\w+$", handle_user)handle_user.get_response({"name": "amir",})Result:
'<p>Hello, amir</p>'
That handler object works, but we can clean it up by using a decorator. Like our
map_over_datafunction above, this decorator doesn't return a function. Instead, it returns aHandlerinstance.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
def handle(path_regex):def wrapper(func):return Handler(path_regex, func)return wrapper - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
handle(r"^/users/\w+$")def handle_user(request):return f"<p>Hello, {request['name']}</p>"handle_user.get_response({"name": "amir",})Result:
'<p>Hello, amir</p>'
That was the second version of this example. The first version explicitly built the
Handler(...), and this version used a decorator. There are two main differences between these versions of the code.First, the decorator version is a bit more terse. We now call
@handle(r"/users/\w+")instead ofhandle_user = Handler(r"/users/\w+", handle_user). This is a minor difference, but it's a bit cleaner, especially when we need to define many handlers.Second, the order of the original code felt "backwards": we defined the function first, and then we said which paths it should handle. The decorator version feels more natural: we say which path we're handling, then define the function that handles that path.
We can define different handlers to handle different paths.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
handle(r"^/users/\w+$")def handle_user(request):return f"<p>Hello, {request['name']}</p>"handle("^/$")def handle_landing(request):return "<p>This is the landing page!</p>" - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
handle_landing.get_response({})Result:
'<p>This is the landing page!</p>'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
handle_user.get_response({"name": "amir",})Result:
'<p>Hello, amir</p>'
Each handler combines a regex with the corresponding function. Together, a set of handlers makes a "routing table". To build a working system, we can write a router that takes an incoming request, checks its path against each handler's regex, and calls the first handler that matches.
This handler example is a simplified version of Flask, a popular Python microframework for routing web requests. Here's "hello world" as a real Flask app:
>
from flask import Flaskapp = Flask(__name__)app.route('/')def index():return 'Hello World!'The
@app.routedecorator here does what our@handledecorator did: associate a path with a handler function. Some of the internal details are different, since Flask is a much more complete tool than our small demo, but the basic idea is the same.Flask code is readable in large part because the decorators cleanly associate a path with its handler. Decorators can be tricky at first, but they're very powerful in real-world code like this!