Python装饰器

Python装饰器使用一个函数去包装另一个函数,本质是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

装饰器的思想,就是把函数中除了正常行为之外的部分抽象出去,这样有很多好处,比如很容易进行代码复用,能遵守科里定律(即一次只做一件事)。

科里定律:一个变量应该代表一样东西,并且只能代表一样东西。它不应该在一种情况下代表这个意思,而在另一种情况下又代表不同的意思。它不能一次代表两样东西。它不能既是地板蜡,又是甜点上的打顶。它应该只有一个含义,并且自始至终只有一个含义。

主要从下面六方面分析:

一、对无参数、无返回值函数装饰;装饰器原理
二、对无参数、有返回值函数进行装饰;多层装饰器
三、对有参数(多参数)、无返回值的函数进行装饰
四、对有参数、有返回值的函数进行装饰
五、使用@functools.wraps(f)正确包装函数、通用装饰器
六、类装饰器
七、总结

一、对无参数、无返回值函数装饰;装饰器原理

先上一个对无参数、无返回值函数装饰的简单例子:

def decoration(func):
    print("...Decorator initializes...")
    def inner(*args):
        print("before function:")
        func()
    print("...The decorator is initialized!...")
    return inner

@ decoration
def say_hello():
    print("hello world!")

say_hello()
# 运行结果:
# ...Decorator initializes...
# ...The decorator is initialized!...
# before function:
# hello world!

原理:先看装饰器函数decoration,该函数接收一个参数func,其实就是接收一个方法名,decoration内部又定义了一个函数inner,decoration的返回值为内部函数inner,其实就是一个闭包函数。

然后,我们在say_hello上一行增加@ decoration, what is this?,当python解释器执行这句话时,会去调用decoration函数,同时将被装饰的函数名作为参数传入,根据闭包的分析,在执行decoration时,会将inner函数返回,同时将其赋值为,此时的say_hello已经不是未加装饰的say_hello了,而是指向decoration.inner函数的地址,相当于:

say_hello=decoration(say_hello)

接下来,在调用say_hello函数时,实质上调用的是decoration.innder函数,至此,就完成了对say_hello的装饰。 –原理至此结束

文章最开头说道,Python装饰器使用一个函数去包装另一个函数,本质是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象,那问题来了,可以使用多重装饰器吗?即在函数已经被一个装饰器1装饰的前提下,再使用一个装饰器2再进行装饰,用多个装饰器装饰同一个对象,答案是肯定的!

而且要注意,多重装饰器的使用顺序为:

装饰时顺序为从内到外,执行时从外到内。

对无参数、无返回值函数装饰:

def decoration1(func):
    print("...Decorator 1 initializes...")
    def inner(*args):
        print("inner 1 is running")
        func()
    print("...The decorator 1 is initialized!...")
    return inner

def decoration2(func):
    print("...Decorator 2 initializes...")
    def inner(*args):
        print("inner 2 is running")
        func()
    print("...The decorator 2 is initialized!...")
    return inner

@ decoration1
@ decoration2
def say_hello():
    print("hello world!")

say_hello()

# 运行结果
# ...Decorator 2 initializes...
# ...The decorator 2 is initialized!...
# ...Decorator 1 initializes...
# ...The decorator 1 is initialized!...
# inner 1 is running
# inner 2 is running
# hello world!

从运行结果可以看到,装饰器的初始化是先2后1,即从内到外,但执行时先1后2,即从外到内,hello_world!还是只输出了一次,为何这样?在下一个例子中进行分析:

中间插一段:python调用函数加括号和不加括号的区别:
print(say_hello)
# <function decoration1.<locals>.inner at 0x0000020D2B9D59D8>
print(say_hello()) # …… None
不带括号时,调用的是这个函数本身 ,是整个函数体,是一个函数对象,不须等该函数执行完成(不加括号表示引用,可理解为一个变量,指向函数代码所在的地址)
带括号,调用的是函数的执行结果,须等该函数执行完成的结果(加括号表示对函数的调用)

二、对无参数、有返回值函数进行装饰;多层装饰器

def decoration1(func):
    print("...Decorator 1 initializes...")
    def inner(*args):
        print("inner 1 is running")
        return '<de1>' + func() + '</de1>'
    print("...The decorator 1 is initialized!...")
    return inner

def decoration2(func):
    print("...Decorator 2 initializes...")
    def inner(*args):
        print("inner 2 is running")
        return '<de2>' + func() + '</de2>'
    print("...The decorator 2 is initialized!...")
    return inner

@ decoration1
@ decoration2
def say_hello():
    print("hello world!")
    return 'say_hello'

print(say_hello())
# 运行结果
# ...Decorator 2 initializes...
# ...The decorator 2 is initialized!...
# ...Decorator 1 initializes...
# ...The decorator 1 is initialized!...
# inner 1 is running
# inner 2 is running
# hello world!
# <de1><de2>say_hello</de2></de1>

对初始化和执行过程进行分析:

  1. 初始化:运行到@decoration1时,会对下一行代码的函数进行装饰,运行到下一行时,发现并不是一个函数名,而是另一个装饰器,此时,装饰器1会暂停执行,而接着执行@decoration2,装饰下一行代码的函数sayhello,相当于执行了代码:say_hello=decoration2(say_hello),执行完毕后再返回去执行@decoration1,相当于执行代码:say_hello=decoration1(decoration2(say_hello)),严谨地说,应该是:decoration2(say_hello)=decoration1(decoration2(say_hello))(还不是特别确定是不是这样),在第五部分我进行详细说明。
  2. 接下来通过inner函数调用say_hello函数,此时say_hello指向的是decoration1.inner函数,因此会先打印“inner 1 is running”,decoration1返回函数中调用decoration2的inner函数,该inner调用的才是原始的say_hello函数,先打印”inner 2 is running”,然后调用say_hello函数,返回”say_hello”,一层层调用结束后,最终输出: <de1><de2>say_hello</de2></de1>。

summary:如果有必要,函数可以被装饰多次,这种情况下,装饰器会引起连锁反应,本质上,每个装饰器的返回值都会传递给上一层的装饰器,直到最顶层。

三、对有参数(多参数)、无返回值的函数进行装饰

def decoration(func):
    print("...Decorator initializes...")
    def inner(names):
        print("inner is running")
        func(name)
    print("...The decorator  is initialized!...")
    return inner

@ decoration
def say_hello(name):
    print('hello '+name)

say_hello('kai')
# 运行结果:
# ...Decorator initializes...
# ...The decorator  is initialized!...
# inner is running
# hello kai

如果原函数有参数,那闭包函数必须保持参数个数一致,并且将参数传递给原方法,如果被修饰的函数有形参,那闭包函数也必须有参数。

如果参数个数为多个或不定呢?用*args和**kwargs即可。

在Python中的代码中经常会见到这两个词 args 和 kwargs,前面通常还会加上一个或者两个星号。其实这只是编程人员约定的变量名字,args 是 arguments 的缩写,表示位置参数;kwargs 是 keyword arguments 的缩写,表示关键字参数。这其实就是 Python 中可变参数的两种形式,并且 *args 必须放在 **kwargs 的前面,因为位置参数在关键字参数的前面。 *args就是就是传递一个可变参数列表给函数实参,这个参数列表的数目未知,甚至长度可以为0,而**kwargs则是将一个可变的关键字参数的字典传给函数实参,同样参数列表长度可以为0或为其他值。 from-简书-Python中的*args和**kwargs

要记住:*args类型是一个tuple,**kwargs是一个dict,且*args只能位于**kwargs前面。

def decoration(func):
    print("...Decorator initializes...")
    def inner(*args,**kwargs):
        print("inner is running")
        func(*args,**kwargs)
    print("...The decorator  is initialized!...")
    return inner

@ decoration
def add(a,b):
    c = a + b
    print("{} + {} = {}".format(a,b,c))

add(1,2)

# 运行结果:
# ...Decorator initializes...
# ...The decorator  is initialized!...
# inner is running
# 1 + 2 = 3

四、对有参数、有返回值的函数进行装饰

def decoration(func):
    print("...Decorator initializes...")
    def inner(name,*args,**kwargs):
        print("inner is running")
        str = func(name,*args,**kwargs)
        return str
    print("...The decorator  is initialized!...")
    return inner

@decoration
def hello(name):
    print("hello world!")
    return "hello "+name

print(hello('kai'))

# 运行结果:
# ...Decorator initializes...
# ...The decorator  is initialized!...
# inner is running
# hello world!
# hello kai

符合预期结果,完成对有参数、又返回值函数的装饰。

五、使用@functools.wraps(f)正确包装函数、通用装饰器

现在再具体研究一下标题二中的多重装饰器,当时说到:

@ decoration1
@ decoration2
def say_hello():

实质上是say_hello = decoration1(decoration2(say_hello)。

我们修改装饰器,使其输出被修饰函数的名字及地址。

def decoration1(func):
    print("...Decorator 1 initializes...")
    def inner(*args):
        print("inner 1 is running")
        print(f'{func} was called.')
        return '<de1>' + func() + '</de1>'
    print("...The decorator 1 is initialized!...")
    return inner

def decoration2(func):
    print("...Decorator 2 initializes...")
    def inner(*args):
        print("inner 2 is running")
        print(f'{func} was called.')
        return '<de2>' + func() + '</de2>'
    print("...The decorator 2 is initialized!...")
    return inner

@ decoration1
@ decoration2
def say_hello():
    print("hello world!")
    return 'say_hello'

print(say_hello())
# 输出信息
# ...Decorator 2 initializes...
# ...The decorator 2 is initialized!...
# ...Decorator 1 initializes...
# ...The decorator 1 is initialized!...
# inner 1 is running
# <function decoration2.<locals>.inner at 0x000002746FAB5A60> was called.
# inner 2 is running
# <function say_hello at 0x000002746FAB59D8> was called.
# hello world!
# <de1><de2>say_hello</de2></de1>

有意思的是,decoration1打印的被修饰函数信息为:

<function decoration2.<locals>.inner at 0x000002746FAB5A60> was called.

而不像decoration2打印出我们期待的被修饰函数信息:

<function say_hello at 0x000002746FAB59D8> was called.

倒是也很好理解,因为装饰器1所装饰的函数并不是say_hello(),而应该是这样的(个人理解):decoration2(say_hello=decoration1(decoration2(say_hello)

作为例子来说这无所谓,但实际上可能会让测试失败,或者引发一些意想不到的错误。

So,如果装饰器的思想是模拟被装饰的函数的行为,那么它也应该模拟被装饰函数的样子。幸运的是,有个python标准库functools模块提供的装饰器wraps能做到这一点。

上代码,做了一点点简化。

import functools
def decoration1(func):
    @functools.wraps(func)
    def inner(*args):
        print(f'{func} was called.')
        func()
    return inner

def decoration2(func):
    @functools.wraps(func)
    def inner(*args):
        print(f'{func} was called.')
    return inner

@ decoration1
@ decoration2
def say_hello():
    print("hello world!")

say_hello()
# 输出信息
# <function say_hello at 0x000002339C4A5A60> was called.
# <function say_hello at 0x000002339C4A59D8> was called.

与之前不加@functools.wraps(func)的打印信息作对比

Now,两个装饰器就都在装饰say_hello函数了,我们的新函数就能很好地起到装饰函数的效果,只是还只能修饰不返回任何值,并且无任何参数的函数,对其改进,让它更通用,就必须负责传递函数参数,并且返回同样的值,这样对其进行修改,并进行验证:

import functools
def decoration(func):
    @functools.wraps(func)  # wraps is a decorator that tells our function to act like f
    def log_f_as_called(*args, **kwargs):
        print(f'{func} was called with arguments={args} and kwargs={kwargs}')
        value = func(*args, **kwargs)
        print(f'{func} return value {value}')
        return value
    return log_f_as_called

@ decoration
def add(a,b):
    c = a + b
    print("{} + {} = {}".format(a,b,c))
    return c

@ decoration
def say_hello():
    print("hello world!")
    return 'a'

add(3,4)
say_hello()
# 运行结果
# <function add at 0x000001FFCB1C5950> was called with arguments=(3, 4) and kwargs={}
# 3 + 4 = 7
# <function add at 0x000001FFCB1C5950> return value 7
# <function say_hello at 0x000001FFCB1C5A60> was called with arguments=() and kwargs={}
# hello world!
# <function say_hello at 0x000001FFCB1C5A60> return value a

现在每次调用都会产生输出,包含函数接收到的所有输入,以及函数的返回值。现在可以用它来修饰任意函数,获得关于函数的输入和输出的调试信息,而用不着手动编写日志代码了。

六、类装饰器

装饰器函数其实是一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。 在python中,一般callable对象都是函数,但是也有例外。对这种例外,我们也有解决办法,就是对该对象重写call方法,那么这个对象就是callable的。

call方法属于魔法方法,魔法方法的“魔力”就体现在能够在需要它的时候自动调用。

改写前:

class Decorator(object):
    def __init__(self, func):
        print('test init')
        print('func name is %s ' % func.__name__)
        self.__func = func
@Decorator
def say_hello():
    print("hello world!")
say_hello()
# 运行结果:
# TypeError: 'Decorator' object is not callable
# test init
# func name is say_hello 

改写call方法后:

class Decorator(object):
    def __init__(self, func):
        print('test init')
        print('func name is %s ' % func.__name__)
        self.__func = func
    def __call__(self, *args, **kwargs):
        print('装饰器中的功能')
        self.__func()
@Decorator
def say_hello():
    print("hello world!")
say_hello()
# 运行结果:
# test init
# func name is say_hello 
# 装饰器中的功能
# hello world!

和之前的原理一样,当python解释器执行到到@Decorator时,会把当前say_hello函数作为参数传入Decorator对象,调用init方法,同时将say_hello函数指向创建的Decorator对象,那么在接下来执行say_hello()的时候,其实就是直接对创建的对象进行调用,执行其call方法。

七、总结

总之,python装饰器是一个非常强大的概念,是python一个非常重要的部分,本质上是一个闭包函数,装饰器的特点有:

  1. 不修改已有函数的源代码;
  2. 不修改已有函数的调用方式;
  3. 给已有函数增加额外的功能。

当模块加载完成后,装饰器就会立即初始化,对已有函数进行装饰,较常见的应用有:

函数执行时间的统计;输出日志信息。

当然,还有很多概念,我还没有学到,后面继续补充吧,像partial函数,wrapper函数、wraps装饰器都还不了解,还有内置装饰器@property,@staticmethod,@classmethod…

参考资料链接:

Decorators in Python: What you need to know

python修饰器(装饰器)以及wraps – 勇敢的公爵 – 博客园

Python中的*args和**kwargs

理解Python闭包概念 – alpha_panda – 博客园

作为程序员,起码要知道的 Python 修饰器!

python中函数加括号与不加括号 – 芦荟~lh – 博客园

发表评论