装饰器

一、什么是装饰器

器:工具

装饰:为被装饰对象添加新功能

装饰器本身可以是任意可调用的对象,即函数

被装饰的对象也可以是任意可调用的对象,也是函数

目标:写一个函数来为另外一个函数添加新功能

二、为何要用装饰器

开放封闭原则:软件一旦上线就应该对修改封闭,对扩展开放

  对修改封闭:

    1、不能修改功能的源代码

    2、也不能修改功能的调用方式

  对扩展开放:

    可以为原有的功能添加新的功能

装饰器就是要在不修改功能源代码以及调用方式的前提下为原功能添加额外新的功能

三、使用装饰器

# 第一步
import time

def index():
    print('欢迎来到index页面')
    time.sleep(3)

start = time.time()
index()
stop = time.time()
print("执行时间是: %s" %(stop - start))
# 如果想多次计算程序运行时间,每次都要写这几步相减操作去计算

#==========================================================================================

# 第二步
import time

def index():
    print('欢迎来到index页面')
    time.sleep(3)

# 现在增加函数,可以通过函数实现多次计算
def wrapper():
    start = time.time()
    index()
    stop = time.time()
    print("执行时间是: %s" %(stop - start))

wrapper()
# 但每次只能计算index, 功能写死了,且修改了程序的执行方式

#==========================================================================================

# 第三步
import time

def index():
    print('欢迎来到index页面')
    time.sleep(3)

func = index    # func = index最原始的内存地址
def wrapper():
    start = time.time()
    func()
    stop = time.time()
    print("执行时间是: %s" %(stop - start))

wrapper()
# 可以计算其他函数了, 但还是修改了程序的执行方式

#==========================================================================================

# 第四步
def index():
    print('欢迎来到index页面')
    time.sleep(3)

# 加上闭包函数,实现传参
def outter():
    func = index    # func = index最原始的内存地址
    def wrapper():
        start = time.time()
        func()
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
    return wrapper

outter()
# 执行outter(), 拿到wrapper的内存地址, 可以将index作为outter的参数

#==========================================================================================

# 第五步

import time

def index():
    print('欢迎来到index页面')
    time.sleep(3)

# 将func作为outter的参数,用于实现传入不同的参数,计算不同程序的运行时间
def outter(func):   # func = index最原始的内存地址
    def wrapper():
        print("hhhhh")
        start = time.time()
        func()
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
    return wrapper

# 将index作为outter的参数, 拿到wrapper的内存地址
index = outter(index)
# 赋值给新的index, 加括号执行
index()
# 实现了一个简单装饰器的功能
 现在想给 index 一个返回值, 要想得到这个返回值,只需要将 index函数的执行结果赋给一个变量,输出这个变量即可

import time

def index():
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

def outter(func):
    def wrapper():
        start = time.time()
        res = func()
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    return wrapper

index = outter(index)
res = index()
print(res)
一个简单的装饰器

但是这样只能输出 index 函数的返回值,功能又写死了,假如其他函数有返回值呢,但你又不知道返回值是什么,于是需要有进一步的操作

import time

def index():
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

def home(name):
    print('欢迎 %s 来到home页面' %name)
    time.sleep(1)

def outter(func):
    # 给wrapper加上参数,只用来接收被装饰函数的参数, 原封不动的转接给最原始的函数
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    return wrapper

index = outter(index)
home = outter(home)
home('qiuxi')
index()
改进一

每次调用 outter 函数再传参,再将 outter 的执行结果赋给新的变量,显得非常麻烦,于是可以有简化操作

import time

def outter(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    return wrapper

# 一执行这行代码, 就会调用outter(),将下方的函数名作为参数传入outter
# 将返回的结果重新赋值给函数名,即 index = outter(index)
@outter
def index():
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

# 这里和上面是相同的操作
# home = outter(home)
@outter
def home(name):
    print('欢迎 %s 来到home页面' %name)
    time.sleep(1)

home('qiuxi')
index()
改进二

因为是计算执行时间的程序,所以我给装饰器换个名字,这一步没什么操作

import time

def timmer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    return wrapper

# 一执行这行, 就会调用timmer()
# timmer将返回的结果重新赋值给函数名
# 即 index = timmer(index)
@timmer
def index():
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

# home = timmer(home)
@timmer
def home(name):
    print('欢迎 %s 来到home页面' %name)
    time.sleep(1)

home('qiuxi')
index()
改进三

写函数的时候,我们要为函数添加注释功能,是在定义函数的下方用三引号写出来,以便于代码的阅读

import time

def timmer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    return wrapper

# @timmer
def index():
    '''这是index功能'''
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

# @timmer
def home(name):
    '''这是home功能'''
    print('欢迎 %s 来到home页面' %name)
    time.sleep(1)

# home('qiuxi')
# index()

# 现在我将装饰器这行注释掉
# 不用装饰器打印的是函数的注释信息
# 现在的需求是加上装饰器还应该是函数的注释信息
# 但这里加上装饰器打印的却是wrapper的注释信息
# 是因为加上装饰器后,index与home指向的都是wrapper
print(help(index))
print(help(home))
改进四

加了装饰器后,这里通过index看到wrapper的注释信息,但现在还是想看到自己函数的注释信息,可以到wrapper里面把自己的 __doc__ 这个功能赋给 wrapper,还有一个功能是 __name__,这个功能是查看对应的函数名,不加装饰器对应的是自己的函数名,加了装饰器对应的是 wrapper 的函数名,现在需求是加了装饰器也是对应自己的函数名,可以到 wrapper 里面把这个功能赋给wrapper

import time

def timmer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    # 把传入的函数的注释信息赋值给wrapper的注释信息
    wrapper.__doc__ = func.__doc__
    # 把传入函数的函数名赋给wrapper
    wrapper.__name__ = func.__name__
    return wrapper


@timmer
def index():
    '''这是index功能'''
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

@timmer
def home(name):
    '''这是home功能'''
    print('欢迎 %s 来到home页面' %name)
    time.sleep(1)

# home('qiuxi')
# index()

print(index.__doc__)
print(index.__name__)
改进五

但一个被装饰的函数显然不仅仅只有这两个功能,那每次都要往 wrapper 添加功能会很麻烦,所以Python提供了一个功能

from functools import wraps
import time

def timmer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" %(stop - start))
        return res
    # 现在想把func下的属性赋值给wrapper
    # Python提供了从functools中导入wraps
    # 在wrapper的上方加一个装饰器wraps, func传入wraps
    # 做的就是把func的一堆属性赋值给wrapper
    return wrapper

@timmer
def index():
    '''这是index功能'''
    print('欢迎来到index页面')
    time.sleep(3)
    return 123

@timmer
def home(name):
    '''这是home功能'''
    print('欢迎 %s 来到home页面' %name)
    time.sleep(1)

# 加了wraps后, 功能都是传入的函数的了
print(help(index))
print(index.__name__)
改进六

至此,无参装饰器便是如此

# 无参装饰器的模版

# 这个func非常固定, 就是用来接收被装饰的函数
def outter(func):
    # *和**就是用来接收被装饰函数的参数, 原封不动的转接给最原始的函数
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        return res
    return wrapper

 应用:用装饰器实现一个登录认证功能

import time
def auth(func):
    def wrapper(*args, **kwargs):
        username = input("请输入用户名: ").strip()
        password = input("请输入密码: ").strip()
        if username == 'egon' and password == '123':
            print("登录成功")
            res = func(*args, **kwargs)
            return res
        else:
            print("用户名或密码错误")
    return wrapper

@auth
def index():
    '''这是index功能'''
    print("欢迎来到index页面")
    time.sleep(2)
    return 123

@auth
def home(name):
    '''这是home功能'''
    print("欢迎 %s 来到home页面" %name)
    time.sleep(1)

index()
home('qiuxi')
登录功能

这个程序用来访问每个功能都需要登录一次,与现实登录情况不符,可以加一个可变类型的全局变量,用来记录登录的状态,如果登录了,就修改它的值,下一次实现不同的功能就进行判断

# 对于不可变类型,可以加一个global,将局部变量变为全局变量
# 但最好不要用gloabl,这对于程序的严谨性并不友好

import time

user_info = {'current_user': None}

def auth(func):
    def wrapper(*args, **kwargs):
        if user_info['current_user'] is not None:
            res = func(*args, **kwargs)
            return res
        username = input("请输入用户名: ").strip()
        password = input("请输入密码: ").strip()
        if username == 'egon' and password == '123':
            # 记录登录状态
            user_info['current_user'] = username

            print("登录成功")
            res = func(*args, **kwargs)
            return res
        else:
            print("用户名或密码错误")
    return wrapper

@auth
def index():
    '''这是index功能'''
    print("欢迎来到index页面")
    time.sleep(2)
    return 123

@auth
def home(name):
    '''这是home功能'''
    print("欢迎 %s 来到home页面" %name)
    time.sleep(1)

index()
home('qiuxi')
改进一

现在想从不同的地方取出用户名和密码, 例如文件、数据库、ldap, 只需要加上判断

import time

user_info = {'current_user': None}

def auth(func):
    def wrapper(*args, **kwargs):
        if user_info['current_user'] is not None:
            res = func(*args, **kwargs)
            return res
        username = input("请输入用户名: ").strip()
        password = input("请输入密码: ").strip()

        # 加上判断, 只需要控制engine就可以让认证功能执行不同的逻辑
        # engine是一个变量, 需要传进来, 函数体需要外部传进来一个值, 有两种方案
        # 一个是参数形式, 这里的engine是wrapper函数内的, 但是engine无法通过wrapper传进来,
        # 因为wrapper接收的参数是原封不动的给func使用, 而func是被装饰的对象, 如果在这里加了engine参数
        # 就是强制让所有被装饰的函数新增engine一个参数, 这显然是不合理的
        # wrapper函数的外部函数还有一个参数func, 但engine通过这个外部函数的参数传进来也是不行的
        # 因为func的功能也很单一, 就是用来接收被装饰对象的, 所以也不能加其他参数

        # 还有一种就是闭包函数形式
        if engine == 'file':
            if username == 'egon' and password == '123':
                # 记录登录状态
                user_info['current_user'] = username

                print("登录成功")
                res = func(*args, **kwargs)
                return res
            else:
                print("用户名或密码错误")
        elif engine == 'mysql':
            print("基于mysql数据库的认证")
        elif engine == 'ldap':
            print("基于ldap的认证")
        else:
            print("无法识别的认证")

    return wrapper

@auth
def index():
    '''这是index功能'''
    print("欢迎来到index页面")
    time.sleep(2)
    return 123

@auth
def home(name):
    '''这是home功能'''
    print("欢迎 %s 来到home页面" % name)
    time.sleep(1)

index()
home('qiuxi')
改进二

可见上面通过直接传参的方式并不能实现功能,于是只能通过闭包函数形式传入 engine

import time

user_info = {'current_user': None}

def auth2(engine = 'file'):
    def auth(func):
        def wrapper(*args, **kwargs):
            if user_info['current_user'] is not None:
                res = func(*args, **kwargs)
                return res
            username = input("请输入用户名: ").strip()
            password = input("请输入密码: ").strip()

            if engine == 'file':
                if username == 'egon' and password == '123':
                    # 记录登录状态
                    user_info['current_user'] = username

                    print("登录成功")
                    res = func(*args, **kwargs)
                    return res
                else:
                    print("用户名或密码错误")
            elif engine == 'mysql':
                print("基于mysql数据库的认证")
            elif engine == 'ldap':
                print("基于ldap的认证")
            else:
                print("无法识别的认证")

        return wrapper
    return auth

# 因为装饰器还是要用auth, 所以执行auth2, 传入engine,
# 拿到原来的auth的地址, 赋给新的auth
auth = auth2(engine = 'file')

@auth
def index():
    '''这是index功能'''
    print("欢迎来到index页面")
    time.sleep(2)
    return 123

@auth
def home(name):
    '''这是home功能'''
    print("欢迎 %s 来到home页面" % name)
    time.sleep(1)

index()
home('qiuxi')
改进四

上面代码简化

import time

user_info = {'current_user': None}

def auth2(engine = 'file'):
    def auth(func):
        def wrapper(*args, **kwargs):
            if user_info['current_user'] is not None:
                res = func(*args, **kwargs)
                return res
            username = input("请输入用户名: ").strip()
            password = input("请输入密码: ").strip()

            if engine == 'file':
                if username == 'egon' and password == '123':
                    # 记录登录状态
                    user_info['current_user'] = username

                    print("登录成功")
                    res = func(*args, **kwargs)
                    return res
                else:
                    print("用户名或密码错误")
            elif engine == 'mysql':
                print("基于mysql数据库的认证")
            elif engine == 'ldap':
                print("基于ldap的认证")
            else:
                print("无法识别的认证")

        return wrapper
    return auth

# auth = auth2(engine = 'file')
# 所以 @auth可以替换成 @auth2(engine = 'file')
# 然后上面的一行 auth = auth2(engine = 'file') 就可以删除了
@auth2(engine = 'file')
def index():
    '''这是index功能'''
    print("欢迎来到index页面")
    time.sleep(2)
    return 123

@auth2(engine = 'file')
def home(name):
    '''这是home功能'''
    print("欢迎 %s 来到home页面" % name)
    time.sleep(1)

index()
home('qiuxi')
改进六

所以最终实现的程序结果是

import time

user_info = {'current_user': None}

def auth2(engine = 'file'):
    def auth(func):
        def wrapper(*args, **kwargs):
            if user_info['current_user'] is not None:
                res = func(*args, **kwargs)
                return res
            username = input("请输入用户名: ").strip()
            password = input("请输入密码: ").strip()

            if engine == 'file':
                print("基于文件的认证")
                if username == 'egon' and password == '123':
                    # 记录登录状态
                    user_info['current_user'] = username

                    print("登录成功")
                    res = func(*args, **kwargs)
                    return res
                else:
                    print("用户名或密码错误")
            elif engine == 'mysql':
                print("基于mysql数据库的认证")
            elif engine == 'ldap':
                print("基于ldap的认证")
            else:
                print("无法识别的认证")
        return wrapper
    return auth

# 程序走到这行, 先不管@符号, 一定是去执行auth2
# 得到一个auth内存地址, 然后再@auth
# 而 @auth就是 auth(index), 即执行auth函数, 这里的index是最原始的index的内存地址
# 得到wrapper的内存地址作为返回值, 然后把返回值赋给新的index
@auth2(engine = 'mysql')
def index():
    '''这是index功能'''
    print("欢迎来到index页面")
    time.sleep(2)
    return 123

@auth2(engine = 'file')
def home(name):
    '''这是home功能'''
    print("欢迎 %s 来到home页面" % name)
    time.sleep(1)

index()
home('qiuxi')
最终版

补充:global 与 nonlocal

global是将局部变量添加global关键字变为全局变量

x = 0
def f1():
    x =1
    def f2():
        x = 2
        def f3():
            global x
            x = 3
            # print(x)
        f3()
    f2()

f1()
print(x)

# 最终输出结果为3
global

nonlocal只能用于局部变量,找上层中离当前函数最近一层的局部变量

声明了nonlocal的内部函数的变量修改会影响到离当前函数最近一层的局部变量

x = 0
def f1():
    x = 1
    def f2():
        # x = 2
        def f3():
            nonlocal x
            print(x)
        f3()
    f2()

f1()

# 程序的执行结果为1
nonlocal

 四、叠加多个装饰器

加载装饰器就是将原函数名换成装饰器最内层的那个函数,在加载完毕后,调用原函数其实就是在调用最内层的函数。

import time

def timmer(func):
    def wrapper1(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print("执行时间是: %s" % (stop - start))
        return res
    return wrapper1

def auth(engine='file'):
    def xxx(func):
        def wrapper2(*args, **kwargs):
            name = input("用户名: ").strip()
            pwd = input("密码: ").strip()
            if engine == 'file':
                print("基于文件的认证")
                if name == "qiuxi" and pwd == "123":
                    print("登录成功")
                    res = func(*args, **kwargs)
                    return res
            elif engine == 'mysql':
                print("基于mysql认证")
            elif engine == 'ldap':
                print("基于ldap的认证")
            else:
                print("用户识别错误")
        return wrapper2
    return xxx

# @auth(engine='file')
@timmer
def index():
    print("欢迎来到index页面")
    time.sleep(2)

index()
View Code

加载顺序就是得到wrapper的过程,执行顺序就是执行wrapper函数的过程

假设只有一个timmer,加载就是@timmer到wrapper1的过程,调用原函数index就是在执行wrapper1函数

当一个被装饰的对象同时叠加多个装饰器的时候,装饰器的加载顺序是自下而上,装饰器最内层的那个函数的执行顺序是自上而下

 加载顺序:

执行顺序即程序的执行顺序,自上而下,逐级细化

 装饰器的先后顺序不同,执行程序的时间也不同

原文地址:https://www.cnblogs.com/qiuxirufeng/p/9715286.html