python装饰器(Decorator)

要学好Python装饰器,需要先掌握以下基础知识:

函数:装饰器本质上是一个函数,因此需要对函数的定义、调用、参数传递等有一定的了解。

闭包:装饰器通常使用闭包来实现,因此需要了解闭包的概念及其使用方法。

类:使用类装饰器,需要熟悉类。

在这些基础上,才能更好地理解解装饰器的语法规则。

python装饰器(Decorator)

python装饰器(decorator)官方定义 【https://docs.python.org/zh-cn/3/glossary.html#term-decorator

返回值为另一个函数的函数,通常使用 @wrapper 语法形式来进行函数变换。

装饰器的常见例子包括 classmethod() 和 staticmethod()。
装饰器语法只是一种语法糖,……

“官言”不好理解,第一次接触看懵不奇怪。特别提示,在学习编程语言知识点时,找到好的示例试一试是很重要的,可以有效加深你的理解,越是不易理解时越要这样做。

在Python 代码中@这个符号是使用装饰器的标识。

语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,不使用也没关系,但是,通常来说使用语法糖能够增加程序的可读性,简化代码,从而减少程序代码出错的机会。

Python的装饰器目的是可以在不改变被装饰函数的代码的情况下给被装饰函数添加新的功能。有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。下面示例说明。

假设我原来有这样一个函数myAdd,计算两个数之和
def myAdd(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

myAdd(10,20) #调用myAdd函数

现在,想为其添加日志:
在函数执行前,先记录一行日志:准备开始执行
在函数执行完,再记录一行日志:执行完

一般这样改:
def myAdd (x, y):
    print("准备开始执行")
    print('{} + {} = {}'.format(x, y, x+y))
    print("执行完")

myAdd(10,20) #调用myAdd函数

但这样,就需要改动了函数myAdd,如何在不改动函数的前提下实现呢?
在原来函数myAdd的前面先定义一个装饰器函数
#定义装饰器函数
def logger(func):
    def wrapper(*args, **kw):
        print("准备开始执行")

        func(*args, **kw)

        print("执行完啦")
    return wrapper 

然后在被装饰的函数前,添加一句:@和装饰器函数名。完整写法如下:

#定义装饰器函数
def logger(func):
    def wrapper(*args, **kw):
        print("准备开始执行")

        func(*args, **kw)

        print("执行完啦")
    return wrapper
    
@logger   #在被装饰的函数前,添加@和装饰器函数名
def myAdd(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

myAdd(10,20) #调用myAdd函数

运行结果:

python3没有urllib python3没有method_decorator_Python

实际上,修改后的完整写法,和下面代码写法等价:

#定义装饰器函数
def logger(func):
    def wrapper(*args, **kw):
        print("准备开始执行")

        func(*args, **kw)

        print("执行完啦")
    return wrapper
    
def myAdd(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

myAdd=logger(myAdd) #留意这一句
myAdd(10,20)

运行结果:

python3没有urllib python3没有method_decorator_python_02

我们通常使用前一种写法。

 总结一下,装饰器(decorator)本身也是一个函数,就像一顶帽子一样戴在被装饰的函数的头上,两者绑定在一起。

装饰器的一般使用形式如下,假设用 funA() 函数——作为装饰器函数,去装饰 funB() 函数——被装饰函数:

#funA 作为装饰器函数
def funA(fn):
    ...

#使用 @ 符号引用funA
@funA
# funB为被装饰函数
def funB():
    ...


上面程序完全等价于下面的程序:

def funA(fn):
    ...

def funB():
    ...

funB = funA(funB)


在一个函数funB上加上@funA ,调用funB就相当于funA(funB),使用 @ 符号引用已有的函数后,可用于修饰其他函数——被修饰的函数。当程序使用“@函数”(比如@ funA)装饰另一个函数(比如 funB)时,实际上完成如下两步:

将被修饰的函数(funB)作为参数传给 @ 符号引用的函数(funA)。

被“@函数”(比如@ funA)修饰的函数(比如 funB)不再是原来的函数,而是被替换成一个新的东西:被修饰的函数总是被替换成 @ 符号所引用的函数的返回值,因此被修饰的函数会变成什么,由@ 符号所引用的函数的返回值决定,如果装饰器函数的返回值为普通变量,那么被修饰的函数名就变成了变量名;如果装饰器返回的是一个函数的名称,那么被修饰的函数名依然表示一个函数。

这些话一开始可能不易理解,下面将给出一个简单示例:

def funA(fn):
    print('A')
    fn() # 执行传入的fn参数
    return 'OK'

@funA
def funB():
    print('B')

print(funB)

运行结果如下:

A
B
OK

上面程序使用 @funA 修饰 funB,这意味着程序要完成两步操作:
将 funB 作为 funA() 的参数,也就是上面代码中 @funA 相当于执行 funA(funB)。
将 funB 替换成上一步执行的结果,funA() 执行完成后返回 OK。 运行上面程序,可以看到如下输出结果:



OK 
请记住,被修饰的函数总是被替换成 @ 符号所引用的函数的返回值,因此被修饰的函数会变成什么,由@ 符号所引用的函数的返回值决定,换句话,当您调用一个修饰函数时,您将得到一个可能与基本定义稍有不同的函数,该函数可能具有其他特性。

@ 符号来修饰函数是 Python 的一个非常实用的功能,它既可以在被修饰函数的前面添加一些额外的处理逻辑,这种改变不需要修改被修饰函数的代码,只要增加一个修饰即可。下面例子示范了如何通过函数装饰器为函数添加额外的处理功能。程序代码如下:

def auth(fn):
    def auth_fn(*args):
        # 用一条语句模拟额外的功能
        print("----这代表额外的处理功能----")
        # 回调要装饰的目标函数
        fn(*args)
    return auth_fn

@auth
def test(a, b):
    c=a+b
    print(" %s + %s = %s " % (a, b, c))

# 调用test()函数,其实是调用装饰后返回的auth_fn函数
test(20, 15)

运行结果如:

----这代表额外的处理功能----
 20 + 15 = 35

装饰器的用法有很多,下面的示例实现对函数计时——打印出func1、func2运行时间,代码如下:

import time

def time_it(func):
      def inner():
           start = time.time()
           func()
           end = time.time()
           print('用时:{}秒'.format(end-start))
      return inner

@time_it
def func1():
      time.sleep(0.5)
      print("Func1 is running.")

@time_it
def func2():
      time.sleep(1.5)
      print("Func2 is running.")

func1() #调用函数
func2()

运行结果类似如:

Func1 is running.
用时:0.5186727046966553秒
Func2 is running.
用时:1.5214245319366455秒

装饰器不仅可以是函数,还可以是类:

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

    def __call__(self,alist):
        list1 = self._func(alist)
        num = 0
        for i in list1:
            num += i
        print("偶数序列为{},其和为{}".format(list1,num))
@AddNum
def pprint(alist):
    list1 = []
    for i in alist:
        if i % 2 == 0:
            list1.append(i)
    return list1
            
pprint([1,2,3,5,6,4,8,11,9])

运行结果如:

偶数序列为[2, 6, 4, 8],其和为20

装饰器还给类添加新的方法、修改类的属性等等。

下面这个简单的例子,演示如何使用装饰器来给类添加新的方法:

# 定义装饰器函数
def add_method(cls):
    def new_method(self, x):
        return x * 2
    cls.double = new_method
    return cls

# 定义被装饰的类
@add_method
class MyClass:
    def __init__(self, x):
        self.x = x

    def square(self):
        return self.x ** 2

# 实例化被装饰的类
obj = MyClass(5)

# 调用原来的方法
result=obj.square()
print(result) # 25

# 调用新添加的double方法
result = obj.double(3)
print(result) # 6

在这个例子中,add_method装饰器接受一个类作为参数,并给这个类添加了一个新的方法double。这个方法接受一个参数x,返回x的两倍。通过在MyClass定义前使用@add_method装饰器,可以在运行时动态地添加方法double。

创建一个MyClass对象,并调用它的double方法。可以看到,我们成功地给MyClass类动态地添加了一个方法double,这个方法可以在运行时进行调用。

由此可知,Python装饰器(Decorator)是一种用于动态修改函数或类的行为的语法结构,可以让代码更加灵活和易于维护

解决装饰器的副作用

Python装饰器(decorator)在实现的时候,有一些细节需要被注意。例如,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变)。例如:

def warfunc(func):
    def warpper(*args,**kwargs):
        print('warpper demo')
        ret = func(*args,**kwargs)
        return ret
    return warpper

@warfunc
def demofunc():
    print('func demo')

demofunc()

print(demofunc)#【注1】

print(demofunc.__name__) #【注2】

运行输出:

warpper demo

func demo

<function warfunc.<locals>.warpper at 0x0000027533B57438>

warpper

其中,【注1】句输出:<function warfunc.<locals>.warpper at 0x0000027533B57438>

其中,【注2】句输出:warpper

说明函数名等函数属性会发生改变,怎么办?

为此,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wraps函数,它能保留原有函数的名称和docstring。

修改上例代码如下:

from functools import wraps #加入此句
def warfunc(func):
    @wraps(func) # @wraps可以让我们在装饰器里面访问在装饰之前的函数的属性。
    def warpper(*args,**kwargs):
        print('warpper demo')
        ret = func(*args,**kwargs)
        return ret
    return warpper

@warfunc
def demofunc():
    print('func demo')

demofunc()

print(demofunc)#【注1】

print(demofunc.__name__) #【注2】

运行输出:

warpper demo

func demo

<function demofunc at 0x0000020B9E467438>

demofunc

其中,【注1】句输出:<function demofunc at 0x0000020B9E467438>

其中,【注2】句输出:demofunc

符合正常期望了。

代码中的@wraps句,可以让我们在装饰器里面访问在装饰之前的函数的属性。

装饰器范本

你可以修改下面的代码段,设计自己的装饰器函数:

from functools import wraps
def decorator_name(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not can_run:
            return "函数将不运行"
        return f(*args, **kwargs)
    return decorated

@decorator_name
def func():
    return("函数运行")

can_run	= True
print(func())#输出:函数正在运行
can_run	= False
print(func()) #	输出:函数将不运行

小结一下

装饰器可以让我们轻松地对函数进行修改和增强,同时保持代码的简洁性和可读性。装饰器本质上是一个 Python 函数或类,该函数或类可以接受一个函数作为参数并返回另一个函数。我们可以用 "@" 符号来调用装饰器对函数进行修饰。

@decorator

def function():

    pass

这样定义之后,function() 函数将会经过 decorator 函数的处理,返回一个新的函数对象,再将该新的函数对象赋值给function,进而改变原有函数的行为。

虽然大多数装饰器都是使用函数实现的,但也可以使用类来实现装饰器。

附录

Python嵌套函数(Nested function)和闭包(closure)

python装饰器(Decorator)再谈

Python 之装饰器http://www.ityouknow.com/python/2019/09/22/Python-decorator-020.html Python装饰器图解 视频讲解 Python装饰器(Decorator) https://www.bilibili.com/video/av25698102

老生常谈Python之装饰器、迭代器和生成器 https://www.jb51.net/article/119590.htm