Expert · Advanced Python · topic 41 of 50 · 35 min
Decorators
Explanation
Functions are objects
In Python a function is a value, just like a number or a list. You can store it in a variable, put it in a list, pass it to another function, and return it from a function. Note the difference: greet is the function itself, greet() calls it.
def greet(name):
return "Namaste, " + name
say = greet # no brackets: we copy the function, we do not call it
print(say("Asha")) # Namaste, Asha
def call_twice(func, value):
return [func(value), func(value)]
print(call_twice(greet, "Ravi"))Inner functions and closures
You can define a function inside another function. The inner function can read the variables of the outer one. When the outer function returns the inner function, the inner function still remembers those variables. A function that remembers variables from where it was created is called a closure.
def make_gst_adder(rate):
def add_gst(price):
return round(price * (1 + rate / 100), 2)
return add_gst
add_18 = make_gst_adder(18)
add_5 = make_gst_adder(5)
print(add_18(100)) # 118.0
print(add_5(100)) # 105.0Writing your first decorator
A decorator is a function that takes a function and returns a new function, usually an inner "wrapper" that does something before and after calling the original. The original function is not changed; it is wrapped.
def announce(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
result = func(*args, **kwargs)
print("Finished", func.__name__)
return result
return wrapper
def add(a, b):
return a + b
add = announce(add) # wrap it by hand
print(add(2, 3))Writing add = announce(add) after every function is untidy, so Python gives a short form. Put @announce on the line above the def. It means exactly the same thing: define the function, then replace it with announce(function).
def announce(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
return func(*args, **kwargs)
return wrapper
@announce
def multiply(a, b):
return a * b
print(multiply(4, 5))After wrapping, the function name becomes "wrapper" and its docstring (the help text in triple quotes) is lost. That confuses error messages, help() and testing tools. The fix is one line: decorate your wrapper with @functools.wraps(func), which copies the name, docstring and other details from the original.
import functools
def announce(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@announce
def refund(amount):
"""Send money back to the customer."""
return amount
print(refund.__name__) # refund (without wraps it would say wrapper)
print(refund.__doc__)Sometimes you want to configure a decorator, as in @repeat(3). Then you need one more layer: repeat(3) is called first and returns the real decorator. It looks complicated, so read it as "a function that makes a decorator".
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return [func(*args, **kwargs) for _ in range(times)]
return wrapper
return decorator
@repeat(3)
def roll():
return "dice rolled"
print(roll())| Use | What the decorator does |
|---|---|
| Timing | Notes the time before and after, and reports how long the call took |
| Logging | Writes the function name and arguments to a log for debugging |
| Caching | Remembers results so a repeated call with the same arguments is instant |
| Login check | In web apps like Flask or Django, refuses the request if the user is not logged in |
| Retry | Calls a flaky network function again a few times before giving up |
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(80)) # instant, thanks to the cache
print(fib.cache_info())Watch the lesson
Decorators · 1/7
This lesson is about decorators, the @ lines you see above functions in Flask, Django and many libraries.
Prefer a video by a teacher? Find free videos on “python decorators explained functools wraps” on YouTube ↗
In real life: A login check on a food delivery app
A food delivery backend has many functions that must only run for a logged-in user: view orders, change address, apply a coupon. Instead of copying the same "is the user logged in?" check into each one, the team writes one decorator and puts it on top of every protected function. Web frameworks such as Flask and Django ship decorators like this.
import functools
def login_required(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("logged_in"):
return "Please log in first"
return func(user, *args, **kwargs)
return wrapper
@login_required
def my_orders(user):
return user["name"] + " has 2 orders"
print(my_orders({"name": "Neha", "logged_in": True}))
print(my_orders({"name": "Guest", "logged_in": False}))Practice (0/3 done)
Write a decorator shout(func). The wrapped function should return the original result in CAPITAL letters. Example: a function that returns "jai hind" should return "JAI HIND" after @shout.
Write a decorator count_calls(func) that counts how many times the decorated function is called. Store the count in an attribute wrapper.calls (start at 0). Use functools.wraps so the function keeps its name.
Write a decorator require_login(func) for a web app. The decorated function always gets a user dict as its first argument. If user.get("logged_in") is not True, raise PermissionError("login required") and do NOT run the function. Otherwise return the function's result.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.