Python 装饰器(优化你的代码)

  • Post author:
  • Post category:python



这里是一段防爬虫文本,请读者忽略。
本文原创首发于CSDN,作者IDYS
博客首页:https://blog.csdn.net/weixin_41633902/
本文链接:https://blog.csdn.net/weixin_41633902/article/details/107952932
未经授权,禁止转载!恶意转载,后果自负!尊重原创,远离剽窃!




写在开头的话

  • 请记住:实践是掌握知识的最快方法
  • 如果你只是怀着看看的态度去快速浏览文章,而不去认认真真的把文章里面讲的任何一个知识点去实践一遍,那么你永远也掌握不了它
  • 生命不息,折腾不止!




Python 装饰器




00. 前述



0.1 需求

  • 一个加法函数,想增强它的功能,能够输出被调用过以及调用的参数信息
def add(x, y):
    return x + y
  • 增加信息输出功能
def add(x, y):
    print("call add, x + y") # 日志输出到控制台 
    return x + y

上面的加法函数是完成了需求,但是有以下的缺点:

  • 打印语句的耦合太高
  • 加法函数属于业务功能,而输出信息的功能,属于非业务功能代码,不该放在业务函数加法中




01. 实现业务分离

  • 做到了业务功能分离,但是

    fn

    函数调用传参是个问题
def add(x,y):
    return x + y
def logger(fn):
    print('begin') # 增强的输出
    x = fn(4,5)
    print('end') # 增强的功能
    return x
print(logger(add))

  • 解决了传参的问题,进一步改变
def add(x,y):
    return x + y
def logger(fn,*args,**kwargs):
    print('begin')
    x = fn(*args,**kwargs)
    print('end')
    return x

print(logger(add,5,y=60))

  • 柯里化
def add(x,y):
    return x + y
def logger(fn):
    def wrapper(*args,**kwargs):
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper

print(logger(add)(5,y=50))

  • 装饰器语法糖
def logger(fn):
    def wrapper(*args,**kwargs):
        print('begin')
        x = fn(*args,**kwargs)
        print('end')
        return x
    return wrapper
@logger # 等价于add = logger(add)
def add(x,y):
    return x + y
print(add(45,40))

  • @logger

    是什么?这就是装饰器语法

  • 演示

def logger(fn):
    def _logger(*args, **kwargs):
        print("start")
        ret = fn(*args, **kwargs)
        print("end")
        return ret
    return _logger


@logger   # fn = logger(fn)
def fn(*args, **kwargs):
    num_sum = 0
    for i in args:
        num_sum += i
    for k, v in kwargs.items():
        num_sum += v
    return num_sum


if __name__ == "__main__":
    print(fn(100, 100, 200, a=234, b=56))
    fn(190, 200, 300)

  • 运行结果
start
end
690
start
end




02. 装饰器特点

  • 装饰器(无参)

    • 它是一个函数
    • 函数作为它的形参
    • 返回值也是一个函数
    • 可以使用

      @functionname

      方式,简化调用
  • 装饰器和高阶函数

    • 装饰器是高阶函数,但装饰器是对传入函数的功能的装饰(功能增强)

  • 演示
import datetime
import time


def logger(fn):
    def _logger(*args, **kwargs):
        print("args={},kwargs={}".format(args, kwargs))
        start_time = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        total_seconds = datetime.datetime.now() - start_time
        print("{} spend{}s".format(fn.__name__, total_seconds.total_seconds()))
        return ret
    return _logger


@logger   # fn = logger(fn)
def fn(*args, **kwargs):
    time.sleep(2)
    num_sum = 0
    for i in args:
        num_sum += i
    for k, v in kwargs.items():
        num_sum += v
    return num_sum


if __name__ == "__main__":
    print(fn(10, 100, 20, x=30, y=50))


  • 运行结果
args=(10, 100, 20),kwargs={'x': 30, 'y': 50}
fn spend2.000723s
210




03. 文档字符串


  • Python

    的文档

    • Python是文档字符串

      Documentation Strings
  • 在函数语句块的第一行,且习惯是多行的文本,所以多使用三引号
  • 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述
  • 可以使用特殊属性

    __doc__

    访问这个文档
def add(x,y): 
    """This is a function of addition"""
    a = x+y 
    return x + y 
print("name={}\ndoc={}".format(add.__name__, add.__doc__)) print(help(add))

  • 副作用
def logger(fn):
    def _logger(*args, **kwargs):
        """
        This is logger function

        :param args:
        :param kwargs:
        :return:
        """
        print("before")
        ret = fn(*args, **kwargs)
        print("after")
        return ret
    return _logger


@logger   # add = logger(add)
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
    print()
    print("*******************")
    print(add(1, 23))

  • 运行结果
name=_logger
doc=
        This is logger function

        :param args:
        :param kwargs:
        :return:
        
Help on function _logger in module __main__:

_logger(*args, **kwargs)
    This is logger function
    
    :param args:
    :param kwargs:
    :return:

None

可以发现原函数的属性全部被替换为装饰器函数的属性了。这使得如果我们想查看原函数的帮助信息和文档信息,最后却看到的是装饰器的帮助信息和文档信息。这样使用装饰器就背道而驰了

  • 改造一下,将装饰器里面的函数的属性替换为传入的函数的属性
def copy_properties(src, dest):   # 复制函数属性
    dest.__name__ = src.__name__
    dest.__doc__ = src.__doc__
    dest.__qualname__ = src.__qualname__


def logger(fn):
    def warpper(*args, **kwargs):
        """
        This is warpper function

        :param args:
        :param kwargs:
        :return:
        """
        print("before")
        ret = fn(*args, **kwargs)
        print("after")
        return ret
    copy_properties(fn, warpper)   # 调用函数属性复制函数
    return warpper


@logger   # add = logger(add) 装饰器
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
  • 运行结果
name=add
doc=
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    
Help on function add in module __main__:

add(*args, **kwargs)
    This is a added function
    
    :param x:
    :param y:
    :return: int
    实现函数的参数相加

None

这种方式成功实现了将原函数的属性复制给了装饰器内部函数。实现了添加了装饰器的功能但是没有改变原来函数的属性。不过,这种方式还不够

  • 通过

    copy_properties

    函数将被包装函数的属性覆盖掉包装函数

  • 凡是被装饰的函数都需要复制这些属性,这个函数很通用

  • 可以将复制属性的函数构建成装饰器函数,带参装饰器




04. 带参装饰器

  • 改装成带参装饰器,实现函数属性的复制
def copy_properties(src):
    def _copy_properties(dest):
        dest.__name__ = src.__name__
        dest.__doc__ = src.__doc__
        dest.__qualname__ = src.__qualname__
        return dest
    return _copy_properties


def logger(fn):
    @copy_properties(fn)   # 通过带参装饰器实现函数复制  warpper = copy_properties(fn)(warpper)
    def warpper(*args, **kwargs):
        """
        This is warpper function

        :param args:
        :param kwargs:
        :return:
        """
        print("before")
        ret = fn(*args, **kwargs)
        print("after")
        return ret
    return warpper


@logger   # add = logger(add)
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
    
  • 运行结果
name=add
doc=
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    
Help on function add in module __main__:

add(*args, **kwargs)
    This is a added function
    
    :param x:
    :param y:
    :return: int
    实现函数的参数相加

None

上面的程序中,通过

@copy_properties(fn)

,带参装饰器。实现了将原函数的属性赋值到目标函数属性上面去




4.1 需求

  • 获取函数的执行时长,对时长超过阈值的函数记录一下

思路:因为要获取函数执行时长,对时长超过阈值的函数记录一下。

  1. 不改变原函数,不对原函数做任何修改。
  2. 因为要对时长超过阈值的函数做记录,那么需要对阈值进行传参
  3. 因为要对函数功能增强,但是又不修改原函数代码。那么则采用装饰器
  4. 因为要增加一个阈值参数,那么则使用带参装饰器
  5. 因为要记录时长那么则导入

    datetime

    模块
  • 代码演示
import datetime
import time


def copy_properties(src):
    def _copy_properties(dest):
        dest.__name__ = src.__name__
        dest.__doc__ = src.__doc__
        dest.__qualname__ = src.__qualname__
        return dest
    return _copy_properties


def logger(timing):
    def _logger(fn):
        @copy_properties(fn)   # 通过带参装饰器实现函数复制  warpper = copy_properties(fn)(warpper)
        def warpper(*args, **kwargs):
            """
            This is warpper function

            :param args:
            :param kwargs:
            :return:
            """
            print("before")
            start_time = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            total_time = (datetime.datetime.now() - start_time).total_seconds()
            print("fast") if total_time < timing else print("slow")
            print("after")
            return ret
        return warpper
    return _logger


@logger(5)   # add = logger(5)(add)
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    time.sleep(6)
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
    add(3, 8)

  • 运行结果
name=add
doc=
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    
Help on function add in module __main__:

add(*args, **kwargs)
    This is a added function
    
    :param x:
    :param y:
    :return: int
    实现函数的参数相加

None
before
slow
after

下面可以推出一个结论。每次要增加一个功能时,那么我们只需要再往外面套一个函数,继续柯里化。这样就可以保证每次虽然增加了函数的功能,但是没有对原函数进行修改。




4.2 带参装饰器的特性

  • 带参装饰器

    • 它是一个函数
    • 函数作为它的形参
    • 返回值是一个不带参的装饰器函数
    • 使用

      @functionname

      (参数列表)方式调用
    • 可以看做在装饰器外层又加了一层函数




4.3 代码继续优化

  • 将记录的功能提取出来,这样就可以通过外部提供的函数来灵活的控制输出

  • 将记录的功能抽取为一个装饰器里面的一个参数,这样就可以随意改变里面的功能

  • 代码演示
import datetime
import time


def copy_properties(src):
    def _copy_properties(dest):
        dest.__name__ = src.__name__
        dest.__doc__ = src.__doc__
        dest.__qualname__ = src.__qualname__
        return dest
    return _copy_properties


def logger(timing,compare=lambda name, timing, program_time: print("{} have take {} seconds".format(name, timing)) if timing > program_time else False ):
    def _logger(fn):
        @copy_properties(fn)   # 通过带参装饰器实现函数复制  warpper = copy_properties(fn)(warpper)
        def warpper(*args, **kwargs):
            """
            This is warpper function

            :param args:
            :param kwargs:
            :return:
            """
            print("before")
            start_time = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            total_time = (datetime.datetime.now() - start_time).total_seconds()
            compare(fn.__name__, total_time, timing)
            print("after")
            return ret
        return warpper
    return _logger


@logger(5)
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    time.sleep(4)
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
    add(3, 8)
  • 运行结果
name=add
doc=
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    
Help on function add in module __main__:

add(*args, **kwargs)
    This is a added function
    
    :param x:
    :param y:
    :return: int
    实现函数的参数相加

None
before
after




05. functools 模块




5.1 functools 模块的普通函数实现属性复制


  • functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)


    • 类似copy_properties功能

    • wrapper

      包装函数、被更新者,

      wrapped

      被包装函数、数据源
    • 元组

      WRAPPER_ASSIGNMENTS

      中是要被覆盖的属性

      __module__



      __name__



      __qualname__



      __doc__



      __annotations__

      模块名、名称、限定名、文档、参数注解
    • 元组

      WRAPPER_UPDATES

      中是要被更新的属性,

      __dict__

      属性字典
    • 增加一个

      __wrapped__

      属性,保留着

      wrapped

      函数

使用

functools



update_wrapper

实现装饰函数到被装饰函数的属性复制

  • 代码演示
import functools


def logger(fn):
    def warpper(*args, **kwargs):
        """
        This is warpper function

        :param args:
        :param kwargs:
        :return:
        """
        print("before")
        ret = fn(*args, **kwargs)
        print("after")
        return ret
    functools.update_wrapper(warpper, fn)
    print("{:^}\t{:^}".format(id(warpper), id(fn)))
    return warpper


@logger
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
    print(id(add.__wrapped__))

  • 运行结果
1696978872496	1696978482512
name=add
doc=
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    
Help on function add in module __main__:

add(x, y)
    This is a added function
    
    :param x:
    :param y:
    :return: int
    实现函数的参数相加

None
1696978482512




5.2 functools 的装饰器方法实现属性复制


  • @functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
  • 类似

    copy_properties

    功能

  • wrapped

    被包装函数
  • 元组

    WRAPPER_ASSIGNMENTS

    中是要被覆盖的属性

    __module__



    __name__



    __qualname__



    __doc__



    __annotations_

    , 模块名、名称、限定名、文档、参数注解
  • 元组

    WRAPPER_UPDATES

    中是要被更新的属性,

    __dict__

    属性字典
  • 增加一个

    __wrapped__

    属性,保留着

    wrapped

    函数

  • 代码演示
import functools


def logger(fn):
    @functools.wraps(fn)
    def warpper(*args, **kwargs):
        """
        This is warpper function

        :param args:
        :param kwargs:
        :return:
        """
        print("before")
        ret = fn(*args, **kwargs)
        print("after")
        return ret
    print("{:^}\t{:^}".format(id(warpper), id(fn)))
    return warpper


@logger
def add(x, y):
    """
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    """
    a = x+y
    return x + y


if __name__ == "__main__":
    print("name={}\ndoc={}".format(add.__name__, add.__doc__))
    print(help(add))
    print(id(add.__wrapped__))

  • 运行结果
2721360226480	2721359836496
name=add
doc=
    This is a added function

    :param x:
    :param y:
    :return: int
    实现函数的参数相加
    
Help on function add in module __main__:

add(x, y)
    This is a added function
    
    :param x:
    :param y:
    :return: int
    实现函数的参数相加

None
2721359836496




写在最后的话:

  • 无论每个知识点的难易程度如何,我都会尽力将它描绘得足够细致
  • 欢迎关注我的CSDN博客,

    IDYS’BLOG

  • 持续更新内容




    linux基础 | 数据通信(路由交换,WLAN) | Python基础 | 云计算
  • 如果你有什么疑问,或者是难题。欢迎评论或者私信我。你若留言,我必回复!
  • 虽然我现在还很渺小,但我会做好每一篇内容。谢谢关注!



版权声明:本文为weixin_41633902原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。