Python学习之Primer_on_Python_Decorators
Primer on Python Decorators
https://realpython.com/primer-on-python-decorators/
Table of Contents
1 |
|
1 | Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written |
Updates:
08/22/2018: Major update adding more examples and more advanced decorators
01/12/2016: Updated examples to Python 3 (v3.5.1) syntax and added a new example
11/01/2015: Added a brief explanation on the functools.wraps() decorator
Functions
Before you can understand decorators, you must first understand how functions work.
For our purposes, a function returns a value based on the given arguments.
Here is a very simple example:
1 | >>> def add_one(number): |
In general, functions in Python may also have side effects rather than just turning an input into an output. The print() function is a basic example of this: it returns None while having the side effect of outputting something to the console. However, to understand decorators, it is enough to think about functions as something that turns given arguments into a value.
Note: In functional programming, you work (almost) only with pure functions without side effects. While not a purely functional language, Python supports many of the functional programming concepts, including functions as first-class objects.
First-Class Objects
In Python, functions are first-class objects. This means that functions can be passed around and used as arguments, just like any other object (string, int, float, list, and so on). Consider the following three functions:
1 | def say_hello(name): |
greet_bob(say_hello)
‘Hello Bob’
greet_bob(be_awesome)
‘Yo Bob, together we are the awesomest!’
1 | Note that greet_bob(say_hello) refers to two functions, but in different ways: greet_bob() and say_hello. The say_hello function is named without parentheses. This means that only a reference to the function is passed. The function is not executed. The greet_bob() function, on the other hand, is written with parentheses, so it will be called as usual. |
def parent():
print(“Printing from the parent() function”)
def first_child():
print("Printing from the first_child() function")
def second_child():
print("Printing from the second_child() function")
second_child()
first_child()
1 | What happens when you call the parent() function? Think about this for a minute. The output will be as follows: |
parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function
1 | Note that the order in which the inner functions are defined does not matter. Like with any other functions, the printing only happens when the inner functions are executed. |
Traceback (most recent call last):
File “
NameError: name ‘first_child’ is not defined
1 | Whenever you call parent(), the inner functions first_child() and second_child() are also called. But because of their local scope, they aren’t available outside of the parent() function. |
def parent(num):
def first_child():
return “Hi, I am Emma”
def second_child():
return "Call me Liam"
if num == 1:
return first_child
else:
return second_child
1 | Note that you are returning first_child without the parentheses. Recall that this means that you are returning a reference to the function first_child. In contrast first_child() with parentheses refers to the result of evaluating the function. This can be seen in the following example: |
first = parent(1)
second = parent(2)
first
<function parent..first_child at 0x7f599f1e2e18>
second
<function parent..second_child at 0x7f599dad5268>
1 | The somewhat cryptic output simply means that the first variable refers to the local first_child() function inside of parent(), while second points to second_child(). |
first()
‘Hi, I am Emma’
second()
‘Call me Liam’
1 | Finally, note that in the earlier example you executed the inner functions within the parent function, for instance first_child(). However, in this last example, you did not add parentheses to the inner functions—first_child—upon returning. That way, you got a reference to each function that you could call in the future. Make sense? |
def my_decorator(func):
def wrapper():
print(“Something is happening before the function is called.”)
func()
print(“Something is happening after the function is called.”)
return wrapper
def say_whee():
print(“Whee!”)
say_whee = my_decorator(say_whee)
1 | Can you guess what happens when you call say_whee()? Try it: |
say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.
1 | To understand what’s going on here, look back at the previous examples. We are literally just applying everything you have learned so far. |
say_whee = my_decorator(say_whee)
1 | In effect, the name say_whee now points to the wrapper() inner function. Remember that you return wrapper as a function when you call my_decorator(say_whee): |
say_whee
<function my_decorator..wrapper at 0x7f3c5dfd42f0>
1 | However, wrapper() has a reference to the original say_whee() as func, and calls that function between the two calls to print(). |
from datetime import datetime
def not_during_the_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
else:
pass # Hush, the neighbors are asleep
return wrapper
def say_whee():
print(“Whee!”)
say_whee = not_during_the_night(say_whee)
1 | If you try to call say_whee() after bedtime, nothing will happen: |
say_whee()
1 |
|
def my_decorator(func):
def wrapper():
print(“Something is happening before the function is called.”)
func()
print(“Something is happening after the function is called.”)
return wrapper
@my_decorator
def say_whee():
print(“Whee!”)
1 | So, @my_decorator is just an easier way of saying say_whee = my_decorator(say_whee). It’s how you apply a decorator to a function. |
def do_twice(func):
def wrapper_do_twice():
func()
func()
return wrapper_do_twice
1 | Note: You can name your inner function whatever you want, and a generic name like wrapper() is usually okay. You’ll see a lot of decorators in this article. To keep them apart, we’ll name the inner function with the same name as the decorator but with a wrapper_ prefix. |
from decorators import do_twice
@do_twice
def say_whee():
print(“Whee!”)
1 | When you run this example, you should see that the original say_whee() is executed twice: |
say_whee()
Whee!
Whee!
1 | Free Bonus: Click here to get access to a free "The Power of Python Decorators" guide that shows you 3 advanced decorator |
from decorators import do_twice
@do_twice
def greet(name):
print(f”Hello {name}”)
1 | Unfortunately, running this code raises an error: |
greet(“World”)
Traceback (most recent call last):
File ““, line 1, in
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given
1 | The problem is that the inner function wrapper_do_twice() does not take any arguments, but name="World" was passed to it. |
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice
1 | The wrapper_do_twice() inner function now accepts any number of arguments and passes them on to the function it decorates. Now both your say_whee() and greet() examples works: |
say_whee()
Whee!
Whee!
greet(“World”)
Hello World
Hello World
1 |
|
from decorators import do_twice
@do_twice
def return_greeting(name):
print(“Creating greeting”)
return f”Hi {name}”
1 | Try to use it: |
hi_adam = return_greeting(“Adam”)
Creating greeting
Creating greeting
print(hi_adam)
None
1 | Oops, your decorator ate the return value from the function. |
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
1 | The return value from the last execution of the function is returned: |
return_greeting(“Adam”)
Creating greeting
Creating greeting
‘Hi Adam’
1 |
|
print.name
‘print’
help(print)
Help on built-in function print in module builtins:
print(…)
1 |
|
say_whee
<function do_twice..wrapper_do_twice at 0x7f43700e52f0>
say_whee.name
‘wrapper_do_twice’
help(say_whee)
Help on function wrapper_do_twice in module decorators:
wrapper_do_twice()
1 |
|
import functools
def do_twice(func):
@functools.wraps(func)
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
1 |
|
say_whee
<function say_whee at 0x7ff79a60f2f0>
say_whee.name
‘say_whee’
help(say_whee)
Help on function say_whee in module whee:
say_whee()
1 |
|
import functools
def decorator(func):
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# Do something before
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_decorator
1 | This formula is a good boilerplate template for building more complex decorators. |
import functools
import time
def timer(func):
“””Print the runtime of the decorated function”””
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
print(f”Finished {func.name!r} in {run_time:.4f} secs”)
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
1 | This decorator works by storing the time just before the function starts running (at the line marked # 1) and just after the function finishes (at # 2). The time the function takes is then the difference between the two (at # 3). We use the time.perf_counter() function, which does a good job of measuring time intervals. Here are some examples of timings: |
waste_some_time(1)
Finished ‘waste_some_time’ in 0.0010 secs
waste_some_time(999)
Finished ‘waste_some_time’ in 0.3260 secs
1 | Run it yourself. Work through the code line by line. Make sure you understand how it works. Don’t worry if you don’t get it, though. Decorators are advanced beings. Try to sleep on it or make a drawing of the program flow. |
import functools
def debug(func):
“””Print the function signature and return value”””
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f”{k}={v!r}” for k, v in kwargs.items()] # 2
signature = “, “.join(args_repr + kwargs_repr) # 3
print(f”Calling {func.name}({signature})”)
value = func(*args, **kwargs)
print(f”{func.name!r} returned {value!r}”) # 4
return value
return wrapper_debug
1 | The signature is created by joining the string representations of all the arguments. The numbers in the following list correspond to the numbered comments in the code: |
@debug
def make_greeting(name, age=None):
if age is None:
return f”Howdy {name}!”
else:
return f”Whoa {name}! {age} already, you are growing up!”
1 | Note how the @debug decorator prints the signature and return value of the make_greeting() function: |
make_greeting(“Benjamin”)
Calling make_greeting(‘Benjamin’)
‘make_greeting’ returned ‘Howdy Benjamin!’
‘Howdy Benjamin!’
make_greeting(“Richard”, age=112)
Calling make_greeting(‘Richard’, age=112)
‘make_greeting’ returned ‘Whoa Richard! 112 already, you are growing up!’
‘Whoa Richard! 112 already, you are growing up!’
make_greeting(name=”Dorrisile”, age=116)
Calling make_greeting(name=’Dorrisile’, age=116)
‘make_greeting’ returned ‘Whoa Dorrisile! 116 already, you are growing up!’
‘Whoa Dorrisile! 116 already, you are growing up!’
1 | This example might not seem immediately useful since the @debug decorator just repeats what you just wrote. It’s more powerful when applied to small convenience functions that you don’t call directly yourself. |
import math
from decorators import debug
Apply a decorator to a standard library function
math.factorial = debug(math.factorial)
def approximate_e(terms=18):
return sum(1 / math.factorial(n) for n in range(terms))
1 | This example also shows how you can apply a decorator to a function that has already been defined. |
approximate_e(5)
Calling factorial(0)
‘factorial’ returned 1
Calling factorial(1)
‘factorial’ returned 1
Calling factorial(2)
‘factorial’ returned 2
Calling factorial(3)
‘factorial’ returned 6
Calling factorial(4)
‘factorial’ returned 24
2.708333333333333
1 | In this example, you get a decent approximation to the true value e = 2.718281828, adding only 5 terms. |
import functools
import time
def slow_down(func):
“””Sleep 1 second before calling the function”””
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
time.sleep(1)
return func(*args, **kwargs)
return wrapper_slow_down
@slow_down
def countdown(from_number):
if from_number < 1:
print(“Liftoff!”)
else:
print(from_number)
countdown(from_number - 1)
1 | To see the effect of the @slow_down decorator, you really need to run the example yourself: |
countdown(3)
3
2
1
Liftoff!
1 | Note: The countdown() function is a recursive function. In other words, it’s a function calling itself. To learn more about recursive functions in Python, see our guide on Thinking Recursively in Python. |
import random
PLUGINS = dict()
def register(func):
“””Register a function as a plug-in”””
PLUGINS[func.name] = func
return func
@register
def say_hello(name):
return f”Hello {name}”
@register
def be_awesome(name):
return f”Yo {name}, together we are the awesomest!”
def randomly_greet(name):
greeter, greeter_func = random.choice(list(PLUGINS.items()))
print(f”Using {greeter!r}”)
return greeter_func(name)
1 | The @register decorator simply stores a reference to the decorated function in the global PLUGINS dict. Note that you do not have to write an inner function or use @functools.wraps in this example because you are returning the original function unmodified. |
PLUGINS
{‘say_hello’: <function say_hello at 0x7f768eae6730>,
‘be_awesome’: <function be_awesome at 0x7f768eae67b8>}
randomly_greet(“Alice”)
Using ‘say_hello’
‘Hello Alice’
1 | The main benefit of this simple plugin architecture is that you do not need to maintain a list of which plugins exist. That list is created when the plugins register themselves. This makes it trivial to add a new plugin: just define the function and decorate it with @register. |
globals()
{…, # Lots of variables not shown here.
‘say_hello’: <function say_hello at 0x7f768eae6730>,
‘be_awesome’: <function be_awesome at 0x7f768eae67b8>,
‘randomly_greet’: <function randomly_greet at 0x7f768eae6840>}
1 | Using the @register decorator, you can create your own curated list of interesting variables, effectively hand-picking some functions from globals(). |
from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(name)
def login_required(func):
“””Make sure user is logged in before proceeding”””
@functools.wraps(func)
def wrapper_login_required(*args, **kwargs):
if g.user is None:
return redirect(url_for(“login”, next=request.url))
return func(*args, **kwargs)
return wrapper_login_required
@app.route(“/secret”)
@login_required
def secret():
…
1 |
|
In this class:
.cylinder_volume() is a regular method.
.radius is a mutable property: it can be set to a different value. However, by defining a setter method, we can do some error testing to make sure it’s not set to a nonsensical negative number. Properties are accessed as attributes without parentheses.
.area is an immutable property: properties without .setter() methods can’t be changed. Even though it is defined as a method, it can be retrieved as an attribute without parentheses.
.unit_circle() is a class method. It’s not bound to one particular instance of Circle. Class methods are often used as factory methods that can create specific instances of the class.
.pi() is a static method. It’s not really dependent on the Circle class, except that it is part of its namespace. Static methods can be called on either an instance or the class.
The Circle class can for example be used as follows:
1 | >>> c = Circle(5) |
Let’s define a class where we decorate some of its methods using the @debug and @timer decorators from earlier:
1 | from decorators import debug, timer |
Using this class, you can see the effect of the decorators:
1 | >>> tw = TimeWaster(1000) |
The other way to use decorators on classes is to decorate the whole class. This is, for example, done in the new dataclasses module in Python 3.7:
1 | from dataclasses import dataclass |
The meaning of the syntax is similar to the function decorators. In the example above, you could have done the decoration by writing PlayingCard = dataclass(PlayingCard).
A common use of class decorators is to be a simpler alternative to some use-cases of metaclasses. In both cases, you are changing the definition of a class dynamically.
Writing a class decorator is very similar to writing a function decorator. The only difference is that the decorator will receive a class and not a function as an argument. In fact, all the decorators you saw above will work as class decorators. When you are using them on a class instead of a function, their effect might not be what you want. In the following example, the @timer decorator is applied to a class:
1 | from decorators import timer |
Decorating a class does not decorate its methods. Recall that @timer is just shorthand for TimeWaster = timer(TimeWaster).
Here, @timer only measures the time it takes to instantiate the class:
1 | >>> tw = TimeWaster(1000) |
Later, you will see an example defining a proper class decorator, namely @singleton, which ensures that there is only one instance of a class.
Nesting Decorators
Nesting Decorators
You can apply several decorators to a function by stacking them on top of each other:
1 | from decorators import debug, do_twice |
Think about this as the decorators being executed in the order they are listed. In other words, @debug calls @do_twice, which calls greet(), or debug(do_twice(greet())):
1 | >>> greet("Eva") |
Observe the difference if we change the order of @debug and @do_twice:
from decorators import debug, do_twice
1 | @do_twice |
In this case, @do_twice will be applied to @debug as well:
1 | >>> greet("Eva") |
Decorators With Arguments
Sometimes, it’s useful to pass arguments to your decorators. For instance, @do_twice could be extended to a @repeat(num_times) decorator. The number of times to execute the decorated function could then be given as an argument.
This would allow you to do something like this:
1 | @repeat(num_times=4) |
1 | >>> greet("World") |
Think about how you could achieve this.
So far, the name written after the @ has referred to a function object that can be called with another function. To be consistent, you then need repeat(num_times=4) to return a function object that can act as a decorator. Luckily, you already know how to return functions! In general, you want something like the following:
1 | def repeat(num_times): |
Typically, the decorator creates and returns an inner wrapper function, so writing the example out in full will give you an inner function within an inner function. While this might sound like the programming equivalent of the Inception movie, we’ll untangle it all in a moment:
1 | def repeat(num_times): |
It looks a little messy, but we have only put the same decorator pattern you have seen many times by now inside one additional def that handles the arguments to the decorator. Let’s start with the innermost function:
1 | def wrapper_repeat(*args, **kwargs): |
This wrapper_repeat() function takes arbitrary arguments and returns the value of the decorated function, func(). This wrapper function also contains the loop that calls the decorated function num_times times. This is no different from the earlier wrapper functions you have seen, except that it is using the num_times parameter that must be supplied from the outside.
One step out, you’ll find the decorator function:
1 | def decorator_repeat(func): |
Again, decorator_repeat() looks exactly like the decorator functions you have written earlier, except that it’s named differently. That’s because we reserve the base name—repeat()—for the outermost function, which is the one the user will call.
As you have already seen, the outermost function returns a reference to the decorator function:
1 | def repeat(num_times): |
There are a few subtle things happening in the repeat() function:
Defining decorator_repeat() as an inner function means that repeat() will refer to a function object—decorator_repeat. Earlier, we used repeat without parentheses to refer to the function object. The added parentheses are necessary when defining decorators that take arguments.
The num_times argument is seemingly not used in repeat() itself. But by passing num_times a closure is created where the value of num_times is stored until it will be used later by wrapper_repeat().
With everything set up, let’s see if the results are as expected:
1 | @repeat(num_times=4) |
1 | >>> greet("World") |
Both Please, But Never Mind the Bread
With a little bit of care, you can also define decorators that can be used both with and without arguments. Most likely, you don’t need this, but it is nice to have the flexibility.
As you saw in the previous section, when a decorator uses arguments, you need to add an extra outer function. The challenge is for your code to figure out if the decorator has been called with or without arguments.
Since the function to decorate is only passed in directly if the decorator is called without arguments, the function must be an optional argument. This means that the decorator arguments must all be specified by keyword. You can enforce this with the special * syntax, which means that all following parameters are keyword-only:
1 | def name(_func=None, *, kw1=val1, kw2=val2, ...): # 1 |
Here, the _func argument acts as a marker, noting whether the decorator has been called with arguments or not:
If name has been called without arguments, the decorated function will be passed in as _func. If it has been called with arguments, then _func will be None, and some of the keyword arguments may have been changed from their default values. The * in the argument list means that the remaining arguments can’t be called as positional arguments.
In this case, the decorator was called with arguments. Return a decorator function that can read and return a function.
In this case, the decorator was called without arguments. Apply the decorator to the function immediately.
Using this boilerplate on the @repeat decorator in the previous section, you can write the following:
1 | def repeat(_func=None, *, num_times=2): |
Compare this with the original @repeat. The only changes are the added _func parameter and the if-else at the end.
Recipe 9.6 of the excellent Python Cookbook shows an alternative solution using functools.partial().
These examples show that @repeat can now be used with or without arguments:
1 | @repeat |
Recall that the default value of num_times is 2:
1 | >>> say_whee() |
Stateful Decorators
Sometimes, it’s useful to have a decorator that can keep track of state. As a simple example, we will create a decorator that counts the number of times a function is called.
Note: In the beginning of this guide, we talked about pure functions returning a value based on given arguments. Stateful decorators are quite the opposite, where the return value will depend on the current state, as well as the given arguments.
In the next section, you will see how to use classes to keep state. But in simple cases, you can also get away with using function attributes:
1 | import functools |
The state—the number of calls to the function—is stored in the function attribute .num_calls on the wrapper function. Here is the effect of using it:
1 | >>> say_whee() |
Classes as Decorators
The typical way to maintain state is by using classes. In this section, you’ll see how to rewrite the @count_calls example from the previous section using a class as a decorator.
Recall that the decorator syntax @my_decorator is just an easier way of saying func = my_decorator(func). Therefore, if my_decorator is a class, it needs to take func as an argument in its .init() method. Furthermore, the class needs to be callable so that it can stand in for the decorated function.
For a class to be callable, you implement the special .call() method:
1 | class Counter: |
The .call() method is executed each time you try to call an instance of the class:
1 | >>> counter = Counter() |
Therefore, a typical implementation of a decorator class needs to implement .init() and .call():
1 | import functools |
say_whee()
Call 1 of ‘say_whee’
Whee!
say_whee()
Call 2 of ‘say_whee’
Whee!
say_whee.num_calls
2
1 |
|
import functools
import time
def slow_down(_func=None, *, rate=1):
“””Sleep given amount of seconds before calling the function”””
def decorator_slow_down(func):
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
time.sleep(rate)
return func(*args, **kwargs)
return wrapper_slow_down
if _func is None:
return decorator_slow_down
else:
return decorator_slow_down(_func)
1 | We’re using the boilerplate introduced in the Both Please, But Never Mind the Bread section to make @slow_down callable both with and without arguments. The same recursive countdown() function as earlier now sleeps two seconds between each count: |
@slow_down(rate=2)
def countdown(from_number):
if from_number < 1:
print(“Liftoff!”)
else:
print(from_number)
countdown(from_number - 1)
1 | As before, you must run the example yourself to see the effect of the decorator: |
countdown(3)
3
2
1
Liftoff!
1 |
|
if _func is None:
return decorator_name
else:
return decorator_name(_func)
1 | Using is returns True only for objects that are the exact same instance. The following @singleton decorator turns a class into a singleton by storing the first instance of the class as an attribute. Later attempts at creating an instance simply return the stored instance: |
import functools
def singleton(cls):
“””Make a class a Singleton class (only one instance)”””
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
if not wrapper_singleton.instance:
wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance
wrapper_singleton.instance = None
return wrapper_singleton
@singleton
class TheOne:
pass
1 | As you see, this class decorator follows the same template as our function decorators. The only difference is that we are using cls instead of func as the parameter name to indicate that it is meant to be a class decorator. |
first_one = TheOne()
another_one = TheOne()
id(first_one)
140094218762280
id(another_one)
140094218762280
first_one is another_one
True
1 |
|
from decorators import count_calls
@count_calls
def fibonacci(num):
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)
1 | While the implementation is simple, its runtime performance is terrible: |
fibonacci(10)
55
fibonacci.num_calls
177
1 | To calculate the tenth Fibonacci number, you should really only need to calculate the preceding Fibonacci numbers, but this implementation somehow needs a whopping 177 calculations. It gets worse quickly: 21891 calculations are needed for fibonacci(20) and almost 2.7 million calculations for the 30th number. This is because the code keeps recalculating Fibonacci numbers that are already known. |
import functools
from decorators import count_calls
def cache(func):
“””Keep a cache of previous function calls”””
@functools.wraps(func)
def wrapper_cache(*args, **kwargs):
cache_key = args + tuple(kwargs.items())
if cache_key not in wrapper_cache.cache:
wrapper_cache.cache[cache_key] = func(*args, **kwargs)
return wrapper_cache.cache[cache_key]
wrapper_cache.cache = dict()
return wrapper_cache
@cache
@count_calls
def fibonacci(num):
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)
1 | The cache works as a lookup table, so now fibonacci() only does the necessary calculations once: |
fibonacci(10)
Call 1 of ‘fibonacci’
…
Call 11 of ‘fibonacci’
55
fibonacci(8)
21
1 |
|
import functools
@functools.lru_cache(maxsize=4)
def fibonacci(num):
print(f”Calculating fibonacci({num})”)
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)
1 | The maxsize parameter specifies how many recent calls are cached. The default value is 128, but you can specify maxsize=None to cache all function calls. However, be aware that this can cause memory problems if you are caching many large objects. |
fibonacci(10)
Calculating fibonacci(10)
Calculating fibonacci(9)
Calculating fibonacci(8)
Calculating fibonacci(7)
Calculating fibonacci(6)
Calculating fibonacci(5)
Calculating fibonacci(4)
Calculating fibonacci(3)
Calculating fibonacci(2)
Calculating fibonacci(1)
Calculating fibonacci(0)
55
fibonacci(8)
21
fibonacci(5)
Calculating fibonacci(5)
Calculating fibonacci(4)
Calculating fibonacci(3)
Calculating fibonacci(2)
Calculating fibonacci(1)
Calculating fibonacci(0)
5
fibonacci(8)
Calculating fibonacci(8)
Calculating fibonacci(7)
Calculating fibonacci(6)
21
fibonacci(5)
5
fibonacci.cache_info()
CacheInfo(hits=17, misses=20, maxsize=4, currsize=4)
1 |
|
def set_unit(unit):
“””Register a unit on a function”””
def decorator_set_unit(func):
func.unit = unit
return func
return decorator_set_unit
1 |
|
import math
@set_unit(“cm^3”)
def volume(radius, height):
return math.pi * radius**2 * height
1 | This .unit function attribute can later be accessed when needed: |
volume(3, 5)
141.3716694115407
volume.unit
‘cm^3’
1 | Note that you could have achieved something similar using function annotations: |
import math
def volume(radius, height) -> “cm^3”:
return math.pi * radius**2 * height
1 | However, since annotations are used for type hints, it would be hard to combine such units as annotations with static type checking. |
import pint
ureg = pint.UnitRegistry()
vol = volume(3, 5) * ureg(volume.unit)
vol
<Quantity(141.3716694115407, ‘centimeter ** 3’)>
vol.to(“cubic inches”)
<Quantity(8.627028576414954, ‘inch ** 3’)>
vol.to(“gallons”).m # Magnitude
0.0373464440537444
1 |
|
def use_unit(unit):
“””Have a function return a Quantity with given unit”””
use_unit.ureg = pint.UnitRegistry()
def decorator_use_unit(func):
@functools.wraps(func)
def wrapper_use_unit(*args, **kwargs):
value = func(*args, **kwargs)
return value * use_unit.ureg(unit)
return wrapper_use_unit
return decorator_use_unit
@use_unit(“meters per second”)
def average_speed(distance, duration):
return distance / duration
1 |
|
bolt = average_speed(100, 9.58)
bolt
<Quantity(10.438413361169102, ‘meter / second’)>
bolt.to(“km per hour”)
<Quantity(37.578288100208766, ‘kilometer / hour’)>
bolt.to(“mph”).m # Magnitude
23.350065679064745
1 |
|
@app.route(“/grade”, methods=[“POST”])
def update_grade():
json_data = request.get_json()
if “student_id” not in json_data:
abort(400)
# Update database
return “success!”
1 | Here we ensure that the key student_id is part of the request. Although this validation works, it really does not belong in the function itself. Plus, perhaps there are other routes that use the exact same validation. So, let’s keep it DRY and abstract out any unnecessary logic with a decorator. The following @validate_json decorator will do the job: |
from flask import Flask, request, abort
import functools
app = Flask(name)
def validate_json(*expected_args): # 1
def decorator_validate_json(func):
@functools.wraps(func)
def wrapper_validate_json(*args, **kwargs):
json_object = request.get_json()
for expected_arg in expected_args: # 2
if expected_arg not in json_object:
abort(400)
return func(*args, **kwargs)
return wrapper_validate_json
return decorator_validate_json
1 |
|
@app.route(“/grade”, methods=[“POST”])
@validate_json(“student_id”)
def update_grade():
json_data = request.get_json()
# Update database.
return “success!”
1 |
|
Geir Arne is an avid Pythonista and a member of the Real Python tutorial team.
More about Geir Arne https://realpython.com/team/gahjelle/
https://github.com/gahjelle