原文链接:https://www.cnblogs.com/tobyqin/p/python-decorator.html

装饰器介绍

1.什么是装饰器

器指的是工具,可以定义成函数,
装饰指的是为其他事物添加额外的东西点缀
合到一起的解释:装饰器指的定义一个函数,该函数是用来为其他函数添加额外的功能,就是拓展原来函数功能的一种函数。

2.为何要用装饰器

开放封闭原则
开放:指的是对拓展功能是开放的
封闭:指的是对修改源代码是封闭的
装饰器就是在不修改被装饰器对象源代码以及调用方式的前提下为被装饰对象添加新功能

假设你的程序实现了say_hello()say_goodbye()两个函数。

def say_hello():
    print("hello")
def say_goodbye():
    print("goodbye")

if __name__ == "__main__":
    say_hello()
    say_goodbye()

但是在实际调用中,我们发现程序出错了,上面的代码打印了两个hello。经过调试你发现是say_goodbye()出错了。老板要求调用每个方法前都要记录进入函数的名称,比如这样:

[DEBUG]: Enter say_hello()
hello
[DEBUG]: Enter say_goodbye()
goodbye

好,小A是个毕业生,他是这样实现的。

def say_hello():
    print("[DEBUG]: Enter say_hello()")
    print("hello")
def say_goodbye():
    print("[DEBUG]: Enter say_goodbye()")
    print("goodbye")

if __name__ == "__main__":
    say_hello()
    say_goodbye()

很low吧? 嗯是的。小B工作有一段时间了,他告诉小A可以这样写。

import inspect

def debug():
    caller_name = inspect.stack()[1][3]
    print("[DEBUG]: Enter {}()".format(caller_name))

def say_hello():
    debug()
    print("hello")

def say_goodbye():
    debug()
    print("goodbye")

if __name__ == "__main__":
    say_hello()
    say_goodbye()

是不是好一点?那当然,但是每个业务函数里都要调用一下debug()函数,是不是很难受?万一老板说say相关的函数不用debug,do相关的才需要呢?
那么装饰器这时候应该登场了。

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能。

3.怎么写一个装饰器

在早些时候 (Python Version < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。

def debug(func):
    def wrapper():
        print("[DEBUG]: Enter {}()".format(func.__name__))
        return func()
    return wrapper

def say_hello():
    print("hello")

def say_goodbye():
    print("goodbye")

if __name__ == "__main__":
    say_hello = debug(say_hello)
    say_goodbye = debug(say_goodbye)
    say_hello()
    say_goodbye()

上面的debug函数其实已经是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。因为这样写实在不太优雅,在后面版本的Python中支持了@语法糖,下面代码等同于早期的写法。

def debug(func):
    def wrapper():
        print("[DEBUG]: Enter {}()".format(func.__name__))
        return func()
    return wrapper

@debug
def say_hello():
    print("hello")

@debug
def say_goodbye():
    print("goodbye")

if __name__ == "__main__":
    say_hello()
    say_goodbye()

这是最简单的装饰器,但是有一个问题,如果被装饰的函数需要传入参数,那么这个装饰器就坏了。因为返回的函数并不能接受参数,你可以指定装饰器函数wrapper接受和原函数一样的参数,比如:

def debug(func):
    # 指定参数
    def wrapper(something):
        print("[DEBUG]: Enter {}()".format(func.__name__))
        return func(something)
    # 返回包装过的函数
    return wrapper

@debug
def say_hello(something):
    print("hello {}".format(something))

@debug
def say_goodbye(something):
    print("goodbye {}".format(something))

if __name__ == "__main__":
    say_hello("")
    say_goodbye("")

这样你就解决了一个问题,但又多了N个问题。因为函数有千千万,你只管你自己的函数,别人的函数参数是什么样子,鬼知道?还好Python提供了可变参数*args和关键字参数**kwargs,有了这两个参数,装饰器就可以用于任意目标函数了。

def debug(func):
    # 可变参数*args和关键字参数**kwargs
    def wrapper(*args, **kwargs):
        print("[DEBUG]: Enter {}()".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@debug
def say_hello(something):
    print("hello {}".format(something))

@debug
def say_goodbye(something):
    print("goodbye {}".format(something))

if __name__ == "__main__":
    say_hello("")
    say_goodbye("")

至此,你已完全掌握初级的装饰器写法。

4.高级一点的装饰器

带参数的装饰器和类装饰器属于进阶的内容。在理解这些装饰器之前,最好对函数的闭包和装饰器的接口约定有一定了解。

4.1带参数的装饰器

假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么装饰器就会是这样的。

def logging(level):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print("[{level}]: Enter function {func}()".format(level=level, func=func.__name__))
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@logging(level="INFO")
def say_hello(something):
    print("hello {}".format(something))

@logging(level="DEBUG")

def say_goodbye(something):
    print("goodbye {}".format(something))

# 如果没有使用语法糖,等同于
# say_goodbye = logging(level="DEBUG")(say_goodbye)

if __name__ == "__main__":
    say_hello("")
    say_goodbye("")

是不是有一些晕?你可以这么理解,当带参数的装饰器被打在某个函数上时,比如@logging(level='DEBUG'),它其实是一个函数,会马上被执行,只要这个它返回的结果是一个装饰器时,那就没问题。细细再体会一下。

4.2基于类实现的装饰器

装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

class Test():
    def __call__(self):
        print("call me")
t = Test()
t()

__call__这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。

回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文)。那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果。

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

    def __call__(self, *args, **kwargs):
        print("[DEBUG]: Enter function {func}()".format(func=self.func.__name__))
        return self.func(*args, **kwargs)

@logging
def say_hello(something):
    print("hello {}".format(something))

say_hello("")
4.3带参数的类装饰器

如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载__call__方法是就需要接受一个函数并返回一个函数。

class logging(object):
    def __init__(self,level="INFO"):
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("[{level}]: Enter function {func}()".format(level=self.level,func=func.__name__))
            func(*args, **kwargs)
        return wrapper

@logging(level="INFO")
def say_hello(something):
    print("hello {}".format(something))

say_hello("")

@logging(level="DEBUG")
def say_goodbye(something):
    print("goodbye {}".format(something))
say_goodbye("")
4.4内置的装饰器

内置的装饰器和普通的装饰器原理是一样的,只不过返回的不是函数,而是类对象,所以更难理解一些。

@property
在了解这个装饰器前,你需要知道在不使用装饰器怎么写一个属性。

def getx(self):
    return self._x

def setx(self,value):
    self._x = value

def delx(self):
    del self._x

x = property(getx,setx,delx,"I am doc for x property")

以上就是一个Python属性的标准写法,其实和Java挺像的,但是太罗嗦。有了@语法糖,能达到一样的效果但看起来更简单。

@property
def x(self):

# 等同于
def x(self):

x = property(x)

属性有三个装饰器:setter, getter, deleter ,都是在property()的基础上做了一些封装,因为setterdeleterproperty()的第二和第三个参数,不能直接套用@语法。getter装饰器和不带getter的属性装饰器效果是一样的,估计只是为了凑数,本身没有任何存在的意义。经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。

>>> property()                                                                                                          
<property object at 0x000001EB894B3188> 

@staticmethod,@classmethod
有了@property装饰器的了解,这两个装饰器的原理是差不多的。@staticmethod返回的是一个staticmethod类对象,而@classmethod返回的是一个classmethod类对象。他们都是调用的是各自的__init__()构造函数。

class classmethod(object):
    """
    classmethod(function) -> method
    """
    def __init__(self,function):
        pass

class staticmethod(object):
    """
    staticmethod(function) -> method
    """
    def __init__(self,function):
        pass

装饰器的@语法就等同调用了这两个类的构造函数。

class Foo(object):
    @staticmethod
    def bar():
        pass
    # 等同于 bar = staticmethod(bar)

至此,我们上文提到的装饰器接口定义可以更加明确一些,装饰器必须接受一个callable对象,其实它并不关心你返回什么,可以是另外一个callable对象(大部分情况),也可以是其他类对象,比如property。

5.优化思路

需求:在不修改index函数的源代码以及调用方式的前提下为其添加统计运行时间的功能

import time
def index(x, y):
    time.sleep(3)
    print('index %s %s' % (x, y))


index(111, 222)
# index(y=111,x=222)
# index(111,y=222)

解决方案一:失败

# 问题:没有修改被装饰对象的调用方式,但是修改了其源代码
import time
def index(x,y):
    start = time.time()
    time.sleep(3)
    print("index %s %s" % (x, y))
    stop = time.time()
    print(stop - start)
index(111,222)

解决方案二:失败

# 问题:没有修改被装饰对象的调用方式,也没有修改了其源代码,并且加上了新功能,但是代码冗余
import time
def index(x,y):
    time.sleep(3)
    print("index %s %s" % (x, y))

start = time.time()
index(111,222)
stop = time.time()
print(stop - start)

解决方案三:失败

# 问题:解决了方案二代码冗余问题,但带来一个新问题即函数的调用方式改变了
import time
def index(x,y):
    time.sleep(3)
    print("index %s %s" % (x, y))

def wrapper():
    start = time.time()
    index(111,222)
    stop = time.time()
    print(stop - start)
wrapper()

方案三的优化一:将index的参数写活了

import time
def index(x,y,z):
    time.sleep(3)
    print("index %s %s %s" % (x,y,z))

def wrapper(*args, **kwargs):
    start = time.time()
    index(*args, **kwargs)
    stop = time.time()
    print(stop - start)
wrapper(3333,4444,5555)

方案三的优化二:在优化一的基础上把被装饰对象写活了,原来只能装饰index

import time
def index(x,y,z):
    time.sleep(3)
    print("index %s %s %s" % (x,y,z))

def home(name):
    time.sleep(2)
    print("welcome %s to home page" % (name))

def outer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        stop = time.time()
        print(stop - start)
    return wrapper

index = outer(index)
home = outer(home)

home('zhang')
index('1','2','3')

方案三的优化三:将wrapper做的跟被装饰对象一模一样,以假乱真

import time
def index(x,y,z):
    time.sleep(3)
    print("index %s %s %s" % (x,y,z))

def home(name):
    time.sleep(2)
    print("welcome %s to home page" % (name))

def outer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print(stop - start)
        return res
    return wrapper

index = outer(index)
home = outer(home)

# home('zhang')
# index('1','2','3')
print("返回值: ", home('zhang'))
print("返回值: ", index('1','2','3'))

那么我们思考如何方案三的基础上不改变函数的调用方式?
代码如下:

import time
# 装饰器
def timmer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print(stop - start)
        return res
    return wrapper

# 语法糖
@timmer
def index(x,y,z):
    time.sleep(3)
    print("index %s %s %s" % (x,y,z))

@timmer
def home(name):
    time.sleep(2)
    print("welcome %s to home page" % (name))

home('zhang')
index('1','2','3')

总结无参装饰器模板

def outer(func):
    def wrapper(*args, **kwargs):
        # 1.调用原函数
        # 2.为其增加新功能
        res = func(*args, **kwargs)
        return res
    return wrapper

为什么说这个就是无参装饰器模板呢?
我们用一个例子来说明:
需求 :写个认证功能(输入账号密码,进行认证),当然账号密码是要从文件中取出。

def auth(func):
    def wrapper(*args, **kwargs):
        # 1.调用原函数
        # 2.为其增加新功能
        name = input("name: ").strip()
        pswd = input("password: ").strip()
        if name == "zhang" and pswd == "123":
            res = func(*args, **kwargs)
            return res
        else:
            print("error")
    return wrapper

@auth
def index():
    print("from index")

index()

参考练习题1:
"""
1.被装饰的函数有返回值
2.如何保留被装饰函数的函数名和帮助信息
"""

import random
import string
import time
import functools

li = [random.choice(string.ascii_letters) for i in range(100)]
# print(li)

# 定义时间计时器
def timec(func):
    @functools.wraps(func)
    # args 元组 kwargs 字典
    def wrapper(*args, **kwargs):
        # 函数执行前
        start_time = time.time()
        res = func(*args, **kwargs)
        # 函数执行后
        end_time = time.time()
        print("运行时间为: %.6f" %(end_time - start_time))
        return res
    return wrapper
    
@timec
def con_add():
    """con_add函数"""
    s = ''
    for i in li:
        s += (i + ',')
    print(s)

@timec
def join_add():
    """join_add函数"""
    print(','.join(li))

con_add()
join_add()

print(con_add.__doc__)
print(con_add.__name__)

print(join_add.__doc__)
print(join_add.__name__)

@timec
def func_list(n):
    return [2 * i for i in range(n)]

@timec
def func_map(n):
    return list(map(lambda x:x*2,range(n)))

print(func_list(1000))
print(func_map(1000))

参考练习题2:
"""
创建装饰器, 要求如下:
创建add_log装饰器, 被装饰的函数打印日志信息;
日志格式为: [字符串时间] 函数名: xxx, 运行时间:xxx, 运行返回值结果:xxx
"""

import time
import functools

def add_log(func):
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        start_time = time.time()
        res = func(*args,**kwargs)
        end_time = time.time()
        print("[%s] 函数名: %s, 运行时间: %.6f, 运行返回值结果: %d" %(time.ctime(),func.__name__,end_time-start_time,res))
        return res
    return wrapper

@add_log
def add(x,y):
    time.sleep(1)
    return x + y
add(1,2)

参考练习题3:
"""
判断是否为管理员root登陆,如果为root登陆,则显示相应的信息
inspect.getcallargs返回一个字典,key值:形参 value值:对应的实参
"""

import inspect
import functools

def is_admin(func):
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        inspect_res = inspect.getcallargs(func,*args,**kwargs)
        print("inspect的返回值: %s" %inspect_res)
        if inspect_res.get("name") == "root":
            res = func(*args,**kwargs)
            return res
        else:
            print("Permission denied")
    return wrapper

@is_admin
def add_student(name):
    print("添加信息")
def del_student(name):
    print("删除信息")
add_student("root")

参考练习题4:
"""
创建装饰器, 要求如下:
创建add_log装饰器, 被装饰的函数打印日志信息;
日志格式为: [字符串时间] 函数名: xxx, 运行时间:xxx, 运行返回值结果:xxx
"""

import time
import functools

def log_kind(kind):
    def add_log(func):
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            start_time = time.time()
            res = func(*args,**kwargs)
            end_time = time.time()
            print("<%s>[%s] 函数名: %s, 运行时间: %.6f, 运行返回值结果: %s" %(kind,time.ctime(),func.__name__,end_time-start_time,res))
            return res
        return wrapper
    return add_log

@log_kind('debug')
def add(x,y):
    time.sleep(1)
    return x + y
print(add(1,2))

参考练习题5:
"""
编写装饰器required_types, 条件如下:
1). 当装饰器为@required_types(int,float)确保函数接收到的每一个参数都是int或者float类型;
2). 当装饰器为@required_types(list)确保函数接收到的每一个参数都是list类型;
3). 当装饰器为@required_types(str,int)确保函数接收到的每一个参数都是str或者int类型;
4). 如果参数不满足条件, 打印 TypeError:参数必须为xxxx类型
"""

import functools
# 设置装饰器接受任意多个参数
def required_types(*kind):
    # 定义函数,接收参数
    def required(func):
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            # 遍历所有参数
            for i in args:
                # 判断每个参数的类型是否为函数中给定的并且传到装饰器的参数
                if not isinstance(i,kind):
                    # 不是,报错
                    # print('TypeError: 参数必须为: ',kind)
                    # 或者将报错部分写为
                    raise TypeError("参数必须为 %s %s" % kind)
            else:
                res = func(*args,**kwargs)
                return res
        return wrapper
    # 给装饰器返回指定的多个参数
    return required

@required_types(int, float)
def add(a,b):
    return a + b 
print(add([1,2,3,4],2.2))
posted on 2022-07-07 15:22  jiayou111  阅读(36)  评论(0编辑  收藏  举报