Order of decorators python

Python Decorators

Sometimes you want to modify an existing function without changing its source code. A common example is adding extra processing (e.g. logging, timing, etc.) to the function.

That’s where decorators come in.

A decorator is a function that accepts a function as input and returns a new function as output, allowing you to extend the behavior of the function without explicitly modifying it.

Every decorator is a form of metaprogramming.

Metaprogramming is about creating functions and classes whose main goal is to manipulate code (e.g., modifying, generating, or wrapping existing code).

Python Functions

Before you can understand decorators, you must first know how functions work.

Pass Function as Arguments

In Python, functions are first-class objects. This means that they can be passed as arguments, just like any other object (string, int, float, list etc.).

If you have used functions like map or filter before, then you already know about it.

Consider the following example.

def hello1(): print("Hello World") def hello2(): print("Hello Universe") def greet(func): func() greet(hello1) # Prints Hello World greet(hello2) # Prints Hello Universe

Here, hello1() and hello2() are two regular functions and are passed to the greet() function.

Note that these functions are passed without parentheses. This means that you are just passing their reference to greet() function.

Inner Functions

Functions can be defined inside other functions. Such functions are called Inner functions. Here’s an example of a function with an inner function.

def outer_func(): def inner_func(): print("Running inner") inner_func() outer_func() # Prints Running inner

Returning a Function

Python also allows you to return a function. Here is an example.

def greet(): def hello(name): print("Hello", name) return hello greet_user = greet() greet_user("Bob") # Prints Hello Bob

Again, the hello() function is returned without parentheses. This means that you are just returning its reference.

And this reference is assigned to greet_user , due to which you can call greet_user as if it were a regular function.

Simple Decorator

Now that you’ve learned that functions are just like any other object in Python, you are now ready to learn decorators.

Let’s see how decorators can be created in Python. Here is a simple example decorator that does not modify the function it decorates, but rather prints a message before and after the function call.

def decorate_it(func): def wrapper(): print("Before function call") func() print("After function call") return wrapper def hello(): print("Hello world") hello = decorate_it(hello) hello() # Prints Before function call # Prints Hello world # Prints After function call

Whatever function you pass to decorate_it() , you get a new function that includes the extra statements that decorate_it() adds. A decorator doesn’t actually have to run any code from func , but decorate_it() calls func part way through so that you get the results of func as well as all the extras.

Simply put, a decorator is a function that takes a function as input, modifies its behavior and returns it.

The so-called decoration happens when you call the decorator and pass the name of the function as an argument.

Here you applied the decorator manually.

Syntactic Sugar

As an alternative to the manual decorator assignment above, just add @decorator_name before the function that you want to decorate.

The following example does the exact same thing as the first decorator example:

def decorate_it(func): def wrapper(): print("Before function call") func() print("After function call") return wrapper @decorate_it def hello(): print("Hello world") hello() # Prints Before function call # Prints Hello world # Prints After function call

So, @decorate_it is just an easier way of saying hello = decorate_it(hello).

Decorating Functions that Takes Arguments

Let’s say you have a function hello() that accepts an argument and you want to decorate it.

def decorate_it(func): def wrapper(): print("Before function call") func() print("After function call") return wrapper @decorate_it def hello(name): print("Hello", name) hello("Bob") # Prints _wrapper() takes 0 positional arguments but 1 was given

Unfortunately, running this code raises an error. Because, the inner function wrapper() does not take any arguments, but we passed one argument.

The solution is to include *args and **kwargs in the inner wrapper function. The use of *args and **kwargs is there to make sure that any number of input arguments can be accepted.

Let’s rewrite the above example.

def decorate_it(func): def wrapper(*args, **kwargs): print("Before function call") func(*args, **kwargs) print("After function call") return wrapper @decorate_it def hello(name): print("Hello", name) hello("Bob") # Prints Before function call # Prints Hello Bob # Prints After function call

Returning Values from Decorated Functions

What if the function you are decorating returns a value? Let’s try that quickly:

def decorate_it(func): def wrapper(*args, **kwargs): print("Before function call") func(*args, **kwargs) print("After function call") return wrapper @decorate_it def hello(name): return "Hello " + name result = hello("Bob") print(result) # Prints Before function call # Prints After function call # Prints None

Because the decorate_it() doesn’t explicitly return a value, the call hello(«Bob») ended up returning None.

To fix this, you need to make sure the wrapper function returns the return value of the inner function.

Let’s rewrite the above example.

def decorate_it(func): def wrapper(*args, **kwargs): print("Before function call") result = func(*args, **kwargs) print("After function call") return result return wrapper @decorate_it def hello(name): return "Hello " + name result = hello("Bob") print(result) # Prints Before function call # Prints After function call # Prints Hello Bob

Preserving Function Metadata

Copying decorator metadata is an important part of writing decorators.

When you apply a decorator to a function, important metadata such as the name, doc string, annotations, and calling signature are lost.

For example, the metadata in our example would look like this:

def decorate_it(func): def wrapper(): print("Before function call") func() print("After function call") return wrapper @decorate_it def hello(): '''function that greets''' print("Hello world") print(hello.__name__) # Prints wrapper print(hello.__doc__) # Prints None print(hello) # Prints .wrapper at 0x02E15078>

To fix this, apply the @wraps decorator from the functools library to the underlying wrapper function.

from functools import wraps def decorate_it(func): @wraps(func) def wrapper(): print("Before function call") func() print("After function call") return wrapper @decorate_it def hello(): '''function that greets''' print("Hello world") print(hello.__name__) # Prints hello print(hello.__doc__) # Prints function that greets print(hello) # Prints 

Whenever you define a decorator, do not forget to use @wraps, otherwise the decorated function will lose all sorts of useful information.

Unwrapping a Decorator

Even if you’ve applied a decorator to a function, you sometimes need to gain access to the original unwrapped function, especially for debugging or introspection.

Assuming that the decorator has been implemented using @wraps, you can usually gain access to the original function by accessing the __wrapped__ attribute.

from functools import wraps def decorate_it(func): @wraps(func) def wrapper(): print("Before function call") func() print("After function call") return wrapper @decorate_it def hello(): print("Hello world") original_hello = hello.__wrapped__ original_hello() # Prints Hello world

Nesting Decorators

You can have more than one decorator for a function. To demonstrate this let’s write two decorators:

from functools import wraps def double_it(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result * 2 return wrapper def square_it(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result * result return wrapper

You can apply several decorators to a function by stacking them on top of each other.

@double_it @square_it def add(a,b): return a + b print(add(2,3)) # Prints 50

Here, the addition of 2 and 3 was squared first then doubled. So you got the result 50.

result = ((2+3)^2)*2 = (5^2)*2 = 25*2 = 50

Execution order of decorators

The decorator closest to the function (just above the def) runs first and then the one above it.

Let’s try reversing the decorator order:

@square_it @double_it def add(a,b): return a + b print(add(2,3)) # Prints 100

Here, the addition of 2 and 3 was doubled first then squared. So you got the result 100.

result = ((2+3)*2)^2 = (5*2)^2 = 10^2 = 100

Applying Decorators to Built-in Functions

You can apply decorators not only to the custom functions but also to the built-in functions.

The following example applies the double_it() decorator to the built-in sum() function.

from functools import wraps def double_it(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result * 2 return wrapper double_the_sum = double_it(sum) print(double_the_sum([1,2])) # Prints 6

Real World Examples

Let’s look at some real world examples that will give you an idea of how decorators can be used.

Debugger

Let’s create a @debug decorator that will do the following, whenever the function is called.

  • Print the function’s name
  • Print the values of its arguments
  • Run the function with the arguments
  • Print the result
  • Return the modified function for use
from functools import wraps def debug(func): @wraps(func) def wrapper(*args, **kwargs): print('Running function:', func.__name__) print('Positional arguments:', args) print('keyword arguments:', kwargs) result = func(*args, **kwargs) print('Result:', result) return result return wrapper

Let’s apply our @debug decorator to a function and see how this decorator actually works.

@debug def hello(name): return "Hello " + name hello("Bob") # Prints Running function: hello # Prints Positional arguments: ('Bob',) # Prints keyword arguments: <> # Prints Result: Hello Bob

You can also apply this decorator to any built-in function like this:

sum = debug(sum) sum([1, 2, 3]) # Prints Running function: sum # Prints Positional arguments: ([1, 2, 3],) # Prints keyword arguments: <> # Prints Result: 6

Timer

The following @timer decorator reports the execution time of a function. It will do the following:

  • Store the time just before the function execution (Start Time)
  • Run the function
  • Store the time just after the function execution (End Time)
  • Print the difference between two time intervals
  • Return the modified function for use
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print("Finished in secs".format(end-start)) return result return wrapper

Let’s apply this @timer decorator to a function.

@timer def countdown(n): while n > 0: n -= 1 countdown(10000) # Prints Finished in 0.005 secs countdown(1000000) # Prints Finished in 0.178 secs

Источник

Читайте также:  Иван Фубаров
Оцените статью