Python之装饰器


前置知识

*args **kwargs

  • 形参:
    • *args 接收参数组成元组
    • **kwargs 接收参数组成字典
  • 实参:
    • *args 将可迭代对象分解出来作为一个个的值进行传递
    • **kwargs 将可迭代对象分解出来作为一组组的值进行传递
def foo(a, b):
    print(a, b)

def func(*args, **kwargs):
    foo(*args, **kwargs)

func(11, 222)
func(111, b=22)
func(a=1111, b=2222)
func(b=88, a=99)


11 222
111 22
1111 2222
99 88

名称空间与作用域

名称空间的“嵌套”关系是在函数定义阶段,即检测语法的时候确定的

函数对象

即可以把函数当做参数传入,也可以把函数当做返回值返回

函数的嵌套

函数内部又定义了一个函数

def outter():
    def inner():
        pass
    return inner

闭包函数

闭包函数:

  1. 函数内定义了一个新的函数

  2. 函数内的函数,引用了外层函数的变量

def outter(x):
    def inner():
        print(x)
    return inner

inner = outter(1)

什么是函数装饰器

函数装饰器是用来为其他函数增加额外功能的

为什么要用装饰器

开放封闭原则是面向对象原则的核心之一。
开放,指扩展功能是开放的;
封闭,指修改源代码是封闭的;
装饰器就是在不修改被装饰对象源代码以及调用方式的前提下为被装饰对象添加新功能

装饰器的使用

下面以一个小例子来演变演示如何对已开发完成的功能通过装饰器的方式追加新功能

需求:写一个加法运算函数。在不修改加法运算函数源代码及调用方式的基础上增加加法运算时长统计功能。(由于加法计算耗时极短,为方便时长统计更直观,在原需求上增加休眠时间2秒time.sleep(2)

  1. 写一个简单的加法运算函数

    def func_sum(a, b):
        result = a + b
        print(result) 
    
    func_sum(1, 3)

    增加休眠时间2秒

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result) 
    
    func_sum(1, 3)

    即我们只计算这3行代码的执行耗时

    time.sleep(2)
    result = a + b
    print(result)
  2. 分析需求,在统计上面3行代码的运行耗时时长,有两种考虑:

    a. 在加法函数中增加统计代码

    import time
    
    def func_sum(a, b):
        start = time.time()
        time.sleep(2)
        result = a + b
        print(result) 
        stop = time.time()
        print(stop - start)
    
    func_sum(1, 3)
    
    
    2.0047240257263184
    4

    这样的实现方式,虽然实现了运行时长统计功能,也没有变更函数的调用方式,但修改了函数自身的源代码,不满足需求,即开放封闭原则。

    b. 在调用函数的代码处增加统计代码

    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    
    start = time.time()
    func_sum(1, 3)
    stop = time.time()
    print(stop - start)
    
    
    4
    2.003887891769409

    这样的实现方式,虽然实现了运行时长统计功能,即没有改动函数的源代码,也没有修改函数调用方式,但这个加法函数会在多次位置进行调用,每调用一次,就会增加startstopstop-start三行代码,造成代码冗余。

  3. 上面的b的可以实现功能,但有代码重复的可能,这里将可能重复的代码写成函数

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    def time_count():
        start = time.time()
        func_sum(1, 3)
        stop = time.time()
        print(stop - start)
    
    time_count()
    
    
    4
    2.004560708999634

    上面的代码虽然解决了多次调用代码冗余的问题,不但改变了原函数func_sum的调用方式,而且def time_count()函数被写死了—— func_sum(1,3),先做如下调整

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    def time_count(a,b):
        start = time.time()
        func_sum(a,b)
        stop = time.time()
        print(stop - start)
    
    time_count(1,3)

    这里func_sum(a,b)只有两个参数,有可能会有增加参数的可能。并且函数time_count是一个统计某个函数运行时长的功能类,但这里写死了只能处理func_sum(a,b),即需要将函数名写活,也要将参数个数写活。先用*args**kwargs写活参数

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    def time_count(*args,**kwargs):
        start = time.time()
        func_sum(*args,**kwargs)
        stop = time.time()
        print(stop - start)
    
    time_count(1,3)

    然后再将函数名称写活,也就是为time_count这个函数传一个函数名的参数,这里使用闭包函数的概念为time_count函数传参

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    def func1(func_sum):		
    
        def time_count(*args,**kwargs):
            start = time.time()
            func_sum(*args,**kwargs)
            stop = time.time()
            print(stop - start)
        return time_count   # time_count函数原本是全局的函数,为了传入一个处理的函数名而通过被func1包上后,需要将time_count本身即内存地址返回到全局
    
    test = func1(func_sum)
    test(1,3)

    修改之前执行计算统计运行时长是func_sum(1,3),而现在是test(1,3),可以将func1(func_sum)赋值给变量time_count

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    def func1(func_sum):
    
        def time_count(*args,**kwargs):
            start = time.time()
            func_sum(*args,**kwargs)
            stop = time.time()
            print(stop - start)
        return time_count   # time_count函数原本是全局的函数,为了传入一个处理的函数名而通过被func1包上后,需要将time_count本身即内存地址返回到全局
    
    func_sum = func1(func_sum)
    func_sum(1,3)
    
    
    4
    2.0046589374542236
  4. 上面的代码已经实现了在不修改函数的源代码和调用方式,为函数增加运行时长统计的功能。上面例子中的变量及函数名按照python的规范惯例做一下调整替换

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
    
    def outter(func):
    
        def wrapper(*args,**kwargs):
            start = time.time()
            func(*args,**kwargs)
            stop = time.time()
            print(stop - start)
        return wrapper   # time_count函数原本是全局的函数,为了传入一个处理的函数名而通过被func1包上后,需要将time_count本身即内存地址返回到全局
    
    
    func_sum = outter(func_sum)
    func_sum(1,3)
  5. func_sum函数增加返回值,然后执行并打印返回值

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
        return result
    
    def outter(func):
    
        def wrapper(*args,**kwargs):
            start = time.time()
            func(*args, **kwargs)
            stop = time.time()
            print(stop - start)
        return wrapper   
    
    
    func_sum = outter(func_sum)
    res = func_sum(1,3)
    print(res)
    
    
    4
    2.0043609142303467
    None

    return的值为None,需要在wrapper函数中增加return返回值

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
        return result
    
    def outter(func):
    
        def wrapper(*args,**kwargs):
            start = time.time()
            res = func(*args,**kwargs)
            stop = time.time()
            print(stop - start)
            return res
        return wrapper   
    
    
    func_sum = outter(func_sum)
    res = func_sum(1,3)
    print(res)
    
    
    4
    2.004016876220703
    4
  6. 增加减法函数func_sub,为减法函数func_sub添加统计运行耗时的方法

    import time
    
    def func_sum(a, b):
        time.sleep(2)
        result = a + b
        print(result)
        return result
    
    def func_sub(a, b):
        time.sleep(2)
        result = a - b
        print(result)
        return result
    
    def outter(func):
    
        def wrapper(*args,**kwargs):
            start = time.time()
            res = func(*args,**kwargs)
            stop = time.time()
            print(stop - start)
            return res
        return wrapper   
    
    func_sum = outter(func_sum)
    res1 = func_sum(1,3)
    print(res1)
    
    func_sub = outter(func_sub)
    res2 = func_sub(333,111)
    print(res2)
    
    
    4
    2.0013039112091064
    4
    222
    2.0044240951538086
    222

    这样就完整实现了通过装饰器对函数功能增加的需求

  7. 通过简便的方式实现装饰器(语法糖)

    先定义装饰器outter,定义好要被装饰的函数func_sumfunc_sub,在两个函数的上方写代码@outter,表示执行outter函数,将被装饰的函数及参数传入装饰器进行执行。

import time

def outter(func):

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


@outter
def func_sum(a, b):
    time.sleep(2)
    result = a + b
    print(result)
    return result

@outter
def func_sub(a, b):
    time.sleep(2)
    result = a - b
    print(result)
    return result


# func_sum = outter(func_sum)
res1 = func_sum(1,3)
print(res1)

# func_sub = outter(func_sub)
res2 = func_sub(333,111)
print(res2)


2.00314998626709
4
222
2.00166392326355
222

无参装饰器模板

通过上面的学习,整理出下面的无参装饰器模板:

def outter(func):   # 装饰器函数,func为被装饰函数
    def wrapper(*args,**kwargs):
        """被装饰函数前需要添加的内容"""
        res = func(*args,**kwargs)  # 被装饰函数
        """被装饰函数后需要添加的内容"""
        return res
    return wrapper

上面的模板还有弊端,函数的属性还没有改。比如__name____doc__help

最终的无参数装饰器的模板如下:

from functools import wraps

def outter(func):   # 装饰器函数,func为被装饰函数
    @wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
    def wrapper(*args,**kwargs):
        """被装饰函数前需要添加的内容"""
        res = func(*args,**kwargs)  # 被装饰函数
        """被装饰函数后需要添加的内容"""
        return res
    return wrapper

有参装饰器

使用装饰器后:

  • 原函数的参数什么样,wrapper的参数就应该是什么样;
  • 原函数的返回值什么样,wrapper的返回值就应该是什么样;
  • 原函数的属性什么样,wrapper的属性就应该是什么样;
from functools import wraps

def outter(func):   # 装饰器函数,func为被装饰函数
    @wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
    def wrapper(*args,**kwargs):
        """被装饰函数前需要添加的内容"""
        res = func(*args,**kwargs)  # 被装饰函数
        """被装饰函数后需要添加的内容"""
        return res
    return wrapper

基于上面的代码,上面已经介绍wrapper()已经无法增加参数了,但outter()函数是可以增加参数的。

若outter()不作为语法糖去装饰别的函数,是可以增加参数的。若outter()作为语法糖去装饰别的函数,就无法增加参数了,所以不建议给outter()增加参数。

若增加一个参数outter(func,x),当代码执行到语法糖的时候,被语法糖装饰的函数会将函数名以参数的形式传给outter(index),而outter(func,x)新增的参数x没有传值的地方,就会报错

Traceback (most recent call last):
  File "/Users/laobai/test3.py", line 24, in <module>
    @outter
TypeError: outter() missing 1 required positional argument: 'x'

基于这样的前提下,若要想再为wrapper传递别的参数,要如何解决呢?下面进行实例演示

a.实现一个简单的访问首页的函数

def index(x,y):
    print('index -->> %s,%s'%(x,y))

index(1,2)

b.为这个函数增加权限认证,有权限的才能执行这段程序,通过装饰器来实现

通过无参装饰器的模板实现如下:

from functools import wraps

def auth(func):   # 装饰器函数,func为被装饰函数
    @wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
    def wrapper(*args,**kwargs):
        """被装饰函数前需要添加的内容"""
        name = input('your name>>:').strip()
        pwd = input('your password>>:').strip()
        if name == 'laobai' and pwd == '123':
            print('login success')
            res = func(*args,**kwargs)  # 被装饰函数
            """被装饰函数后需要添加的内容"""
            return res
        else:
            print('user or password error')
    return wrapper

@auth
def index(x,y):
    print('index -->> %s,%s'%(x,y))

index(1,2)

上面代码写死了账号信息

执行结果(有权限)

your name>>:laobai
your password>>: 123
login success
index -->> 1,2

执行结果(没权限)

your name>>:lb
your password>>:123
user or password error

c.在上面的代码基础上,增加两个功能home()和navigation()

代码实现如下:

def auth(func):   # 装饰器函数,func为被装饰函数
    @wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
    def wrapper(*args,**kwargs):
        """被装饰函数前需要添加的内容"""
        name = input('your name>>:').strip()
        pwd = input('your password>>:').strip()
        if name == 'laobai' and pwd == '123':
            print('login success')
            res = func(*args,**kwargs)  # 被装饰函数
            """被装饰函数后需要添加的内容"""
            return res
        else:
            print('user or password error')
    return wrapper

@auth
def index(x,y):
    print('index -->> %s,%s'%(x,y))

@auth
def home(name):
    print('home -->> %s'%(name))

@auth
def navigation():
    print('navigation bar')

index(1,2)
home('laobai')
navigation()

执行结果如下:

your name>>:laobai
your password>>:123
login success
index -->> 1,2
your name>>:laobai
your password>>:123
login success
home -->> laobai
your name>>:laobai
your password>>:123
login success
navigation bar

d.上面的程序中写死了有权限的登录名和密码,这里模拟3个功能,分别从三个位置获取用户名和密码:文件、数据库、ldap

from functools import wraps

def auth(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        name = input('your name>>:').strip()
        pwd = input('your password>>:').strip()

        if data_type == 'file':
            print('从文件中获取账号密码进行验证')
            if name == 'laobai' and pwd == '123':
                res = func(*args,**kwargs)  # 被装饰函数
                return res
            else:
                print('user or password error')
        elif data_type == 'mysql':
            print('从mysql数据库中获取账号密码进行验证')

        elif data_type == 'ldap':
            print('从ldap中获取账号密码进行验证')
        else:
            print("不支持该data_type")
    return wrapper

@auth
def index(x,y):
    print('index -->> %s,%s'%(x,y))

@auth
def home(name):
    print('home -->> %s'%(name))

@auth
def navigation():
    print('navigation bar')

index(1,2)
home('laobai')
navigation()

e.上面代码,模拟了从三种不同位置获取账号信息的场景,但引入了新的变量data_type,上面的代码还没有实现传入调用。之前已经确定了outter()也就是这里的auth()及wrapper()均无法再增加传参了。

当一个函数需要一个参数的时候,有两种方案:1.使用传参的形式;2.在外面再包一层函数,即使用装饰器的形式;

from functools import wraps

def auth(data_type):
    def outter(func):
        @wraps(func)
        def wrapper(*args,**kwargs):
            name = input('your name>>:').strip()
            pwd = input('your password>>:').strip()

            if data_type == 'file':
                print('从文件中获取账号密码进行验证')
                if name == 'laobai' and pwd == '123':
                    res = func(*args,**kwargs)  # 被装饰函数
                    return res
                else:
                    print('user or password error')
            elif data_type == 'mysql':
                print('从mysql数据库中获取账号密码进行验证')

            elif data_type == 'ldap':
                print('从ldap中获取账号密码进行验证')
            else:
                print("不支持该data_type")
        return wrapper
    return outter

@auth(data_type='file')
def index(x,y):
    print('index -->> %s,%s'%(x,y))

@auth(data_type='mysql')
def home(name):
    print('home -->> %s'%(name))

@auth(data_type='ldap')
def navigation():
    print('navigation bar')

index(1,2)
home('laobai')
navigation()

执行结果:

your name>>:laobai
your password>>:123
从文件中获取账号密码进行验证
index -->> 1,2
your name>>:laobai
your password>>:123
从mysql数据库中获取账号密码进行验证
your name>>:laobai
your password>>:123
从ldap中获取账号密码进行验证

上面的代码,通过对auth()语法糖中进行传参data_type给auth()函数,代入的新的参数后,再通过装饰器outter为index()做为语法糖@outter

可以简单理解为:

@auth(data_type='file')
def index(x,y):
    print('index -->> %s,%s'%(x,y))

等价于

outter=auth(data_type='file')
@outter
def index(x,y):
    print('index -->> %s,%s'%(x,y))

根据上面,总结有参装饰器模板:

from functools import wraps

def 有参装饰器(x,y,z):
    def outter(func):   # 装饰器函数,func为被装饰函数
        @wraps(func) # 增加wraps语法糖,将原函数的属性赋值给wrapper函数
        def wrapper(*args,**kwargs):
            """被装饰函数前需要添加的内容"""
            res = func(*args,**kwargs)  # 被装饰函数
            """被装饰函数后需要添加的内容"""
            return res
        return wrapper
    return outter

@有参装饰器(1,2,z=3)
def 被装饰函数():
    pass

叠加使用装饰器

def deco1(func1):   # func1 = wrapper2的内存地址
    def wrapper1(*args,**kwargs):
        print("正在运行===>deco1.wrapper1")
        res1 = func1(*args,**kwargs)
        return res1
    return wrapper1

def deco2(func2):   # func2=wrapper3的内存地址
    def wrapper2(*args,**kwargs):
        print("正在运行===>deco2.wrapper2")
        res2 = func2(*args,**kwargs)
        return res2
    return wrapper2

def deco3(x):
    def outter3(func3): # func3=被装饰对象index函数的内存地址
        def wrapper3(*args,**kwargs):
            print("正在运行===>deco3.outter3.wrapper3")
            res3 = func3(*args,**kwargs)
            return res3
        return wrapper3
    return outter3

# 加载顺序是从下到上
@deco1      # index=deco1(wrapper2的内存地址) ===> index=wrapper1的内存地址
@deco2      # index=deco2(wrapper3的内存地址) ===> index=wrapper2的内存地址
@deco3(111) # ===> @outter3 ===> index=outter3(index) ===> index= wrapper3的内存地址
def index(x,y):
    print('from index %s:%s' %(x,y))

# 执行顺序自上而下,即wrapper1 --> wrapper2 --> wrapper3
index(1,3)

执行结果:

正在运行===>deco1.wrapper1
正在运行===>deco2.wrapper2
正在运行===>deco3.outter3.wrapper3
from index 1:3

文章作者: 老百
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 老百 !
 上一篇
Python匿名函数和高阶函数 Python匿名函数和高阶函数
本文结合各种实际的例子详细讲解了Python中匿名函数和5个内建高阶函数的使用,能够帮助理解Python的数据结构和提高数据处理的效率。
2022-10-13
下一篇 
Python之迭代器和生成器 Python之迭代器和生成器
迭代器(iterator)是访问集合内元素的一种方式,提供了一种遍历类序列对象的方法。从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束。对于字典、文件对象、自定义对象类型等,可以自定义迭代方式,从而实现对这些对象的遍历。
2022-10-09
  目录