Easy Tutorial
❮ Android Tutorial 9 Image Verilog2 Tutorial ❯

Python Function Decorators

Category Programming Techniques

Decorators are an important part of Python. Simply put: they are functions that modify the functionality of other functions. They help to make our code more concise and more Pythonic. Most beginners don't know where to use them, so I'm going to share where decorators can make your code more succinct.

First, let's discuss how to write your own decorator.

This may be one of the most difficult concepts to grasp. We will discuss one step at a time so you can fully understand it.

Everything is an Object

First, let's understand functions in Python:

def hi(name="yasoob"):
    return "hi " + name

print(hi())
# output: 'hi yasoob'

# We can even assign a function to a variable, like
greet = hi
# We are not using parentheses here because we are not calling the hi function
# but placing it inside the greet variable. Let's try running this

print(greet())
# output: 'hi yasoob'

# If we delete the old hi function, let's see what happens!
del hi
print(hi())
#outputs: NameError

print(greet())
#outputs: 'hi yasoob'

Defining Functions within Functions

Those were the basic concepts of functions. Let's take your knowledge a step further. In Python, we can define another function within a function:

def hi(name="yasoob"):
    print("now you are inside the hi() function")

    def greet():
        return "now you are in the greet() function"

    def welcome():
        return "now you are in the welcome() function"

    print(greet())
    print(welcome())
    print("now you are back in the hi() function")

hi()
#output: now you are inside the hi() function
#       now you are in the greet() function
#       now you are in the welcome() function
#       now you are back in the hi() function

# The above shows that whenever you call hi(), greet() and welcome() will be called at the same time.
# Then the greet() and welcome() functions cannot be accessed outside the hi() function, for example:

greet()
#outputs: NameError: name 'greet' is not defined

Now we know that we can define other functions within a function. That is to say: we can create nested functions. Now you need to learn a little more, which is that functions can also return functions.

Returning Functions from Functions

There is actually no need to execute another function within a function; we can also return it as an output:

def hi(name="yasoob"):
    def greet():
        return "now you are in the greet() function"

    def welcome():
        return "now you are in the welcome() function"

    if name == "yasoob":
        return greet
    else:
        return welcome

a = hi()
print(a)
#outputs: <function greet at 0x7f2143c01500>

# The above clearly shows that `a` now points to the greet() function in the hi() function
# Now try this

print(a())
#outputs: now you are in the greet() function

Look at this code again. In the if/else statement, we return greet and welcome, not greet() and welcome(). Why is that? Because when you put a pair of parentheses behind it, the function will be executed; however, if you don't put parentheses behind it, it can be passed around and can be assigned to other variables without executing it.

Do you understand? Let me explain a little more detail.

When we write a = hi(), hi() is executed, and since the name parameter defaults to yasoob, the function greet is returned. If we change the statement to a = hi(name = "ali"), then the welcome function will be returned. We can also print out hi()(), which will output now you are in the greet() function.

Passing Functions as Arguments to Another Function

def hi():
    return "hi yasoob!"

def doSomethingBeforeHi(func):
    print("I am doing some boring work before executing hi()")
    print(func())

doSomethingBeforeHi(hi)
#outputs: I am doing some boring work before executing hi()
#        hi yasoob!

Now you have all the necessary knowledge to further learn what decorators really are. Decorators allow you to execute code before and after a function.

Your First Decorator

In the previous example, we actually created a decorator! Now let's modify the This is a Python code snippet that demonstrates the use of decorators in various scenarios, including authorization, logging, and creating parameterized decorators. Below is the English translation of the provided text:


Note: @wraps is a decorator that takes a function and adds features such as copying the function's name, documentation, and argument list. This allows us to access the attributes of the function before it is decorated within the decorator.


Usage Scenarios

Now let's look at where decorators shine particularly and how using them can make some things easier to manage.

Authorization

Decorators can help check if a person is authorized to use an endpoint of a web application. They are widely used in Flask and Django web frameworks. Here is an example of using authorization based on decorators:

from functools import wraps

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated

Logging

Logging is another highlight of the use of decorators. Here is an example:

from functools import wraps

def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logit
def addition_func(x):
   """Do some math."""
   return x + x

result = addition_func(4)
# Output: addition_func was called

I'm sure you're already thinking of another smart use for decorators.


Parameterized Decorators

Consider this question, isn't @wraps also a decorator? But it takes a parameter, just like any ordinary function can do. So why don't we do the same?

This is because when you use the @my_decorator syntax, you are applying a wrapper function that takes a single function as a parameter. Remember, in Python, everything is an object, and this includes functions! With this in mind, we can write a function that returns a wrapper function.

Embedding Decorators in Functions

Let's go back to the logging example and create a wrapper function that allows us to specify a log file for output.

from functools import wraps

def logit(logfile='out.log'):
    def logging_decorator(func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # Open logfile and write to it
            with open(logfile, 'a') as opened_file:
                # Now log to the specified logfile
                opened_file.write(log_string + '\n')
            return func(*args, **kwargs)
        return wrapped_function
    return logging_decorator

@logit()
def myfunc1():
    pass

myfunc1()
# Output: myfunc1 was called
# Now a file called out.log appears with the above string

@logit(logfile='func2.log')
def myfunc2():
    pass

myfunc2()
# Output: myfunc2 was called
# Now a file called func2.log appears with the above string

Decorator Classes

Now we have a logit decorator that can be used in a formal environment, but when some parts of our application are still fragile, exceptions may need more urgent attention. For example, sometimes you just want to log to a file. And sometimes you want to send the issues that catch your attention to an email while also keeping logs for a record. This is a scenario for inheritance, but so far, we have only seen functions used to build decorators.

Fortunately, classes can also be used to build decorators. Let's now rebuild logit as a class instead of a function.

from functools import wraps

class logit(object):
    def __init__(self, logfile='out.log'):
        self.logfile = logfile

    def __call__(self, func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # Open logfile and write
            with open(self.logfile, 'a') as opened_file:
                # Now log to the specified file
                opened_file.write(log_string + '\n')
            # Now, send a notification
            self.notify()
            return func(*args, **kwargs)
        return wrapped_function

    def notify(self):
        # logit only logs, does nothing else
        pass

This implementation has an additional advantage of being neater than the nested function approach, and wrapping a function still uses the same syntax as before:

@logit()

This approach is logically sound, and the functionality is achieved, but when we make the call, we are no longer invoking the actual business logic function foo; instead, we have switched to the use_logging function, which disrupts the original code structure. Now we have to pass the original foo function as an argument to the use_logging function every time. Is there a better way? Of course, the answer is decorators.

**Simple Decorators**

def use_logging(func):

def wrapper():
    logging.warn("%s is running" % func.__name__)
    return func()   # When foo is passed as an argument, executing func() is equivalent to executing foo()
return wrapper

def foo(): print('i am foo')

foo = use_logging(foo) # Since the decorator use_logging(foo) returns the function object wrapper, this statement is equivalent to foo = wrapper foo() # Executing foo() is equivalent to executing wrapper()


use_logging is a decorator; it is a regular function that wraps the function func, which executes the actual business logic, making it appear as if foo has been decorated by use_logging. The use_logging returns a function named wrapper. In this example, the function's entry and exit are referred to as a cross-cutting concern, and this programming approach is known as aspect-oriented programming.

**@ Syntax Sugar**

If you have been working with Python for a while, you must be familiar with the @ symbol. That's right, the @ symbol is the syntax sugar for decorators. It is placed at the beginning of the function definition, which allows us to omit the final step of reassignment.

def use_logging(func):

def wrapper():
    logging.warn("%s is running" % func.__name__)
    return func()
return wrapper

@use_logging def foo(): print("i am foo")

foo()


As shown above, with @, we can omit the line foo = use_logging(foo), and simply call foo() to get the desired result. You see, the foo() function does not need to be modified at all; just add the decorator at the definition, and the call remains the same as before. If we have other similar functions, we can continue to use decorators to embellish the functions without repeatedly modifying the functions or adding new encapsulations. In this way, we have improved the reusability of the program and increased its readability.

The convenience of decorators in Python is attributed to the fact that Python functions can be passed as parameters to other functions like ordinary objects, can be assigned to other variables, can be returned as values, and can be defined within another function.

***args, **kwargs**

Some may ask, what if my business logic function foo requires arguments? For example:

def foo(name): print("i am %s" % name)


We can specify the parameters when defining the wrapper function:

def wrapper(name): logging.warn("%s is running" % func.__name__) return func(name) return wrapper


In this way, the parameters defined by the foo function can be defined within the wrapper function. At this point, someone may ask, what if the foo function receives two arguments? Three arguments? What if I pass many more? When the decorator does not know how many parameters foo has, we can use *args to represent it:

def wrapper(args): logging.warn("%s is running" % func.__name__) return func(args) return wrapper


In this way, no matter how many parameters are defined by foo, I can pass them all to func intact. This does not affect the business logic of foo. At this point, some readers may ask, what if the foo function also defines some keyword arguments? For example:

def foo(name, age=None, height=None): print("I am %s, age %s, height %s" % (name, age, height))


At this time, you can specify the keyword arguments for the wrapper function:

def wrapper(args, *kwargs): # args is an array, kwargs is a dictionary logging.warn("%s is running" % func.__name__) return func(args, *kwargs) return wrapper


**Decorators with Parameters**

Decorators have even greater flexibility, such as decorators with parameters. In the above decorator call, the decorator only accepts one argument, which is the business function foo. The syntax of the decorator allows us to provide other parameters when calling, such as @decorator(a). This provides greater flexibility in writing and using decorators. For example, we can specify the log level in the decorator because different business functions may require different log levels.

def use_logging(level): def decorator(func): def wrapper(args, *kwargs): if level == "warn This is an English translation of the provided Chinese text. Please find the translation below:

# This is another function
pass
# from functools import wraps
# @wraps(anotherFunc)
@Wraps(anotherFunc)
def NewFibonacci(*args, **kwargs):
    print('[%s]func: %s is called, %s' % (level, func.__name__, NewFibonacci.__doc__))
    from time import time
    start = time()
    rst = func(*args, **kwargs)
    end = time()
    print('cost time: %.2fs' % (end - start))
    return rst
return NewFibonacci
return TmpDecorator


@NewDecorator('log')
def Fibonacci_print(num=10):
    if num <= 0:
        return
    minNum, maxNum = 0, 1
    while num > 0:
        tmpNum = minNum
        print(tmpNum, end=',')
        minNum = maxNum
        maxNum += tmpNum
        num -= 1

Fibonacci_print()
print(Fibonacci_print.__name__)

Output is:

[log]func: Fibonacci_print is called, it's another function
0,1,1,2,3,5,8,13,21,34,cost time: 0.07s
anotherFunc

** DragonZ

* 246**[email protected]

-

** Yan Weichao

* yif**[email protected]

Revisiting the implementation principle of the @wraps(func) decorator.

Correct the basic code framework described by the above @DragonZ, it should be as follows:

def Wraps(fWrap):
    def TmpWraps(func):
        def WrapsDecorator(*args, **kwargs):
            return func(*args, **kwargs)
            WrapsDecorator.__name__ = fWrap.__name__
            WrapsDecorator.__doc__ = fWrap.__doc__
        return WrapsDecorator
    return TmpWraps

And this decorator does not just copy the __name__ and __doc__ function metadata, there are more other information, the correct implementation code is as follows:

def mywraps(fwrap):
    def TmpWraps(func):
        def dec(**args):
            return func(**args)
        meta_info = ['__module__', '__name__', '__qualname__', '__doc__', '__annotations__']
        for meta in meta_info:
            setattr(dec, meta, getattr(fwrap, meta))
        # Get the metadata of the fwrap function one by one and copy it to the dec function
        return dec
    return TmpWraps

In addition, regarding class decorators, the following explanation may be more friendly to beginners:

Decorators can also be implemented through classes, which mainly utilize the following characteristics of classes to indirectly implement the function decorator function:

The function call syntax f() is equivalent to the instantiation of the class, that is, calling the class __init__ function to create an object

The object call obj() is equivalent to running the object's __call__ magic function

Using a class to implement a decorator can avoid more than two layers of nested function decorators, because if there are three layers, the outermost function can be considered as calling the class __init__ function, which can make the code more readable and maintainable

In essence, as long as the class's __init__ and __call__ magic functions are implemented, and the decorator parameters are accepted in the __init__ function, and the specific decorator structure is implemented in the __call__ function, it is possible

Here is an example, using a class to implement a parameterized decorator, you can observe the difference

```python from functools import wraps

Define a class for the decorator name

class with_para_decorator: # In the class __init__ function, accept the decorator parameters and assign them to the class instance parameters, so that other functions can use them at any time # Of course, if there are no parameters in the decorator, you can omit a, b here, which is equivalent to class instantiation without parameters def __init__(self, a, b): self.a = a self.b = b # In the class __call__ function, accept the decorated function and specifically define the decorator def __call__(self, func): @wraps(func) def wrap_function(arg1, arg2): print('Decorate a function with parameters, the parameters passed by the function are: {0}, {1}'.format(arg1, arg2)) print('Decorator with parameters, the parameters passed by the decorator are: {0}, {1}'.format(self.a

❮ Android Tutorial 9 Image Verilog2 Tutorial ❯