This is the 18th day of my participation in the August Challenge

In order to share this article, I did a lot of work, so without my consent, please do not reprint, thank you!

In this share, we’ll look at what decorators are and how to create and use them. Decorators provide a simple syntax for calling higher-order functions. By definition, a decorator is a function that takes a function and extends its behavior without changing its behavior. This sounds a bit confusing, but it’s actually not very complicated. Read this article and you will have a thorough understanding of decorators, and then consolidate your results with lots of examples.

Before we get started, let’s do a little example to get a feel for the benefits of decorators, to experience the benefits of decorators.

import time

def slow_square(number) :
    print(f"Sleeping for {number} seconds")
    time.sleep(number)
    return number**2

print(slow_square(3))
print(slow_square(3))
print(slow_square(3))
Copy the code

After executing the above code, simulate a time-consuming operation with sleep, which puts the thread to sleep for 3 seconds. The executing code will print a number every 3 seconds, but this function will print the same result.

Lru_cache (maxsize=128, typed=False).functools. lru_cache(maxsize=128, typed=False) The effect lasts only 3 seconds and then prints three nines in one go.

import time
import functools

@functools.lru_cache(maxsize=128, typed=False)
def slow_square(number) :
    print(f"Sleeping for {number} seconds")
    time.sleep(number)
    return number**2

print(slow_square(3))
print(slow_square(3))
print(slow_square(3))
Copy the code

We printed functools.lru_cache to see that the decorator is a function.

print(functools.lru_cache) #<function lru_cache at 0x108fcd560>
Copy the code

Functions (Functions provides)

Before you can understand decorators, you need to do some preparatory work, first understanding how functions work. You can simply call a function, a function that returns a value based on a given parameter. Here is a very simple example.

def add(num_1,num_2) :
    return num_1 + num_2
Copy the code

In general, functions in Python can also have side effects beyond simply converting input to output. A typical example is the print() function, which returns None and has the side effect of printing to the console. The IO operation can be considered a side effect. However, to understand decorators, it is sufficient to think of a function as something that turns a given parameter into a value.

Note: In functional programming, most functions are pure functions with no side effects. Although Python is not a purely functional language like Haskell, python supports many of the concepts of functional programming, including functions as first-class objects, which, like Javascript, are first-class citizens of the language.

functional

In Python, functions are first class objects. This means that functions can be passed and as arguments like other objects (string, int, float, list, and so on) or as return values.

print_proxy = print
Copy the code

A function can be assigned to a variable

def greet(name,printer=print) :
    printer(f"hi {name}")
Copy the code

We can pass print to greet as an argument

def reverse(text) :
    print(text[::-1])

greet("hi decorations",printer=reverse)
Copy the code

Inline function

We can define nested functions within other functions. Such functions are called inner functions. The following function prefix_factory defines an internal function prefix_print.

def prefix_factory(prefix) :
    def prefix_print(text) :
        print(f"{prefix}: {text}")
    return prefix_print
Copy the code

Function as the return value

For higher-order functions, you can return the function as a return value.

def prefix_factory(prefix) :
    def prefix_print(text) :
        print(f"{prefix}: {text}")
    return prefix_print
Copy the code

Simple decorator

Now that you have seen that functions are just like any other object in Python, we have some understanding of Python functions, so let’s start writing a Python decorator.

def before_and_after(func) :
    def wrapper(text) :
        print("BEFORE")
        func(text)
        print("AFTER")

    return wrapper
Copy the code
def greet(text) :
    print(f"hi {text}") 

greet = before_and_after(greet)
Copy the code
greet("decorators")
Copy the code
BEFORE
hi decorators
AFTER
Copy the code

Syntactic sugar

The way you decorate before_and_after() above is a little clumsy. Clearly not an elegant way to implement the greet function name. In addition, decorations are hidden beneath the function definition.

Python, however, allows you to use decorators with the @ symbol in a much simpler way, the way we’re familiar with syntactic sugar. The greet function is the same as the greet function using a syntactic sugar.

def before_and_after(func) :
    def wrapper(text) :
        print("BEFORE")
        func(text)
        print("AFTER")

    return wrapper

@before_and_after
def greet(text) :
    print(f"hi {text}")

greet("decorators")
Copy the code

Reuse decorator

To recap, the decorator is just a normal Python function. All the usual reusable tools are available. Let’s move the decorator into its own module, which can be used in many other functions.

Create a file called decorators.py with the following contents.

import random

def do_twice(func) :
    def wrapper(*args,**kwargs) :
        val_1 = func(*args,**kwargs)
        val_2 = func(*args,**kwargs)
        return (val_1,val_2)
    return wrapper

@do_twice
def roll_dice() :
    return random.randint(1.6)


print(roll_dice())
Copy the code

A great convenience when working with Python, especially in an interactive shell, is its great introspection. Introspection refers to an object’s ability to understand its own properties at run time. For example, a function knows its own name and documentation.

>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:
Copy the code
def do_twice(func) :
    def wrapper_do_twice(*args, **kwargs) :
        res_1 = func(*args, **kwargs)
        res_2 = func(*args, **kwargs)
        return res_1, res_2
    return wrapper_do_twice
@do_twice
def guess_number() :
    return random.randint(1.6)
Copy the code
guess_number.__name__ #'wrapper_do_twice'
Copy the code
help(guess_number)
Copy the code

When the function guess_number() is decorated, guess_number() changes its identity to itself, which is a bit consufing. When guess_number.__name__ is called to see the identity of the function, the output is not guess_number but the wrapper_DO_TWICE () internal function in the do_TWICE () decorator. There is no problem, but we want to return guess_number instead of · wrapper_do_twice()

To solve this problem, the decorator should use the @functools.wraps decorator so that no information about the decorated function is retained.

def do_twice(func) :
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs) :
        res_1 = func(*args, **kwargs)
        res_2 = func(*args, **kwargs)
        return res_1, res_2
    return wrapper_do_twice
Copy the code
@do_twice def guess_number(): return random. Randint (1,6)Copy the code
guess_number.__name__ #'guess_number'
Copy the code

Some examples of applying decorators

Let’s look at a few more useful examples of decorators. You’ll notice that they mostly follow the same patterns you’ve learned so far.

The timer

def timer(func) :
    @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:4.f} secs")
        return value
    return wrapper_timer
Copy the code
@timer
def slow_square(number) :
    time.sleep(number)
    return number**2
Copy the code
slow_square(3)
Copy the code
Finished 'slow_square' in 3.0003 secs
9
Copy the code

The decorator will output the execution time of the decorator function. The decorator will output the execution time of the decorator function.

Note: If you just want to know the running time of your function, the @Timer decorator implemented above is a good choice. If you want a more accurate measure of code execution time, @timer might not be enough, and you should consider the timeit module in the standard library. Temporarily disable garbage collection and run multiple experiments to eliminate noise from function calls.

A debugging tool

Next, write a debug decorator. The @debug decorator prints the parameters of the function and its return value each time the function is called.

def debug(func) :
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs) :
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v! r}" for k,v in kwargs.items()]
        
        signatre = ",".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signatre})")
        
        value = func(*args,**kwargs)
        print(f"{func.__name__! r} returned {value! r}")
        return value
    return wrapper_debug
Copy the code
  • Create a list of positional parameters throughrepr()Get an elegant string representing each parameter
  • Create a list of keyword arguments.f-stringFormat each parameter askey=value, in which the! rThe specifier is represented byrepr()To represent the value
@debug
def slow_square(number) :
    time.sleep(number)
    return number**2
Copy the code
slow_square(3)
Copy the code
Calling slow_square(3)
'slow_square' returned 9
9
Copy the code

slow

The following example may not seem very useful. Why do you want Python code to slow down? The most common use case might be for a continuous check on a resource such as the download progress, where this method might come in handy.

def slow_down(func) :
    @functools.wraps(func)
    def wrapper_slow_donw(*args, **kwargs) :
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_donw
Copy the code
@slow_down
def countdown(from_num) :
    if from_num < 1:
        print("done")
    else:
        print(from_num)
        countdown(from_num - 1)
Copy the code

Registered machine

Decorators don’t have to wrap the function they’re decorating. They can also simply register that a function exists and return it unwrapped. This can be used, for instance, to create a light-weight plug-in architecture:

Decorators do not necessarily wrap the functions they decorate. You can also simply register a function to the collection and return the function directly. For example, this can be used to create a lightweight plug-in architecture.

PLUGINS = dict(a)def register(func) :
    PLUGINS[func.__name__] = func
    return func

@register
def java_dev(name) :
    return f"my name is {name} and I am Java Developer"

@register
def python_dev(name) :
    return f"my name is {name} and I am Python Developer"

def randomly_greet(name) :
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter! r}")
    return greeter_func(name)
Copy the code

The @Register decorator simply stores a reference to the decorated function in the global PLUGINS dict. Note that in this case, you don’t have to write a wrap function or use @functools.wraps because you are returning the original function without any modifications

The randomly_greet() function randomly selects a registered function to use. Notice that the PLUGINS dictionary already contains references to each function object registered as a plug-in.

randomly_greet("mike")
Copy the code
Using 'java_dev'
'my name is mike and I am Java Develope
Copy the code