Decorators for getting started with Python

Before I talk about Python decorators, I want to give an example, which is a bit dirty, but very relevant to the topic of decorators.

The main function of the underwear that everyone has is to cover shame, but in winter, it can't protect us from the wind and cold, what should we do? One way we thought of was to transform the underwear to make it thicker and longer, so that it not only has the function of covering shame, but also provides warmth, but there is a problem, after we transformed the underwear into trousers , although it still has a shading function, it is no longer a real pair of underwear in essence. So smart people invented trousers, and without affecting the underwear, they directly put the trousers on the outside of the underwear, so that the underwear is still the underwear, and the baby is no longer cold after the trousers. The decorator, like the trousers we are talking about here, provides our body with a warm effect without affecting the function of the underwear.

 

Before talking about decorators, we must first understand one thing. Functions in Python are different from Java and C++. Functions in Python can be passed as parameters to another function like ordinary variables. For example:  

def foo():
    print("foo")

def bar(func):
    func()

bar(foo)

Back to our topic officially. A decorator is essentially a Python function or class, which allows other functions or classes to add extra functionality without any code modification. The return value of the decorator is also a function/class object. It is often used in scenarios with faceted requirements, such as: log insertion, performance testing, transaction processing, caching, permission verification, etc. The decorator is an excellent design to solve such problems. With decorators, we can extract a lot of identical code that has nothing to do with the function itself into decorators and continue to reuse them. In a nutshell, the role of a decorator is to add extra functionality to an existing object.

Let's start with a simple example, although the actual code may be a lot more complicated than this:

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

Now there is a new requirement, I hope to record the execution log of the function, so add the log code to the code:

def foo():
    print('i am foo')
    logging.info("foo is running")

What if the functions bar() and bar2() have similar requirements? Write another record in the bar function? This results in a lot of identical code. In order to reduce repeated code writing, we can do this and redefine a new function: specialize in processing logs, and then execute the real business code after the logs are processed.

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

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

use_logging(foo)

This is logically no problem, the function is implemented, but when we call it, we no longer call the real business logic foo function, but replace it with the use_logging function, which destroys the original code structure, now we I have to pass the original foo function as a parameter to the use_logging function every time, so is there a better way? Of course there is, and the answer is decorators.

 

simple decorator

def use_logging(func):

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

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

foo = use_logging(foo)   #Because 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 an ordinary function, it wraps the function func that executes the real business logic in it, it looks like foo is decorated by use_logging, the return of use_logging is also a function, the name of this function is wrapper. In this example, when the function enters and exits, it is called a cross-section, and this kind of programming is called aspect-oriented programming.

 

@ syntactic sugar

If you have been in contact with Python for a while, you must be familiar with the @ symbol. Yes, the @ symbol is the syntactic sugar of the decorator. It is placed at the beginning of the function definition, so that you can omit the operation of reassignment in the last step. .

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 foo = use_logging(foo)this sentence and call foo() directly to get the desired result. Did you see that the foo() function does not need to be modified, just add a decorator where it is defined, and the call is the same as before. If we have other similar functions, we can continue to call the decorator to decorate function without repeatedly modifying the function or adding new wrappers. In this way, we improve the reusability of the program and increase the readability of the program.

Decorators are so convenient to use in Python because Python functions can be passed as parameters to other functions like ordinary objects, can be assigned to other variables, can be used as return values, and can be defined in another function.

 

*args、**kwargs

Someone may ask, what if my business logic function foo needs parameters? for example

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

We can specify 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 in the wrapper function. At this time, someone has to ask, what if the foo function accepts two parameters? What about three parameters? What's more, I may pass many. When the decorator doesn't know how many arguments foo has, we can use *args instead:

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

如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:

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

这时,你就可以把 wrapper 函数指定关键字函数:

def wrapper(*args, **kwargs):
        # args是一个数组,kwargs一个字典
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

 

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_logging(level="warn")
def foo(name='foo'):
    print("i am %s" % name)

foo()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

# @use_logging(level="warn")等价于@decorator

 

类装饰器

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')

@Foo
def bar():
    print ('bar')

bar()

 

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__、参数列表,先看例子:  

# 装饰器
def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'with_logging'
        print func.__doc__       # 输出 None
        return func(*args, **kwargs)
    return with_logging

# 函数
@logged
def f(x):
   """does some math"""
   return x + x * x

logged(f)

不难发现,函数 f 被with_logging取代了,当然它的docstring__name__就是变成了with_logging函数的信息了。好在我们有functools.wrapswraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'f'
        print func.__doc__       # 输出 'does some math'
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

 

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

f = a(b(c(f)))

 

 

参考:https://foofish.net/python-decorator.html

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324653246&siteId=291194637