zoukankan      html  css  js  c++  java
  • Python 元编程

    Python 中提供了一个叫装饰器的特性,用于在不改变原始对象的情况下,增加新功能或行为。

    这也属于 Python "元编程" 的一部分,在编译时一个对象去试图修改另一个对象的信息,实现 "控制一切" 目的。

    本篇文章作为装饰器的基础篇,在阅读后应该了解如下内容:

    • 装饰器的原理?
    • 装饰器如何包裹有参数的函数?
    • 装饰器本身需要参数怎么办?
    • 被装饰器修饰的函数还是原函数吗,怎么解决?
    • 装饰器嵌套时的顺序?
    • 装饰器常见的应用场景?

    装饰器原理

    在具体装饰器的内容前,先来回顾下 Python 中的基本概念:

    1. Python 中,一切都是对象,函数自然也不例外

    python 中的对象都会在内存中用于属于自己的一块区域。在操作具体的对象时,需要通过 “变量” ,变量本身仅是一个指针,指向对象的内存地址。

    函数作为对象的一种,自然也可以被变量引用。

    def hello(name: str):
        print('hello', name)
    hello('Ethan')
    
    alias_func_name = hello
    alias_func_name('Michael')
    # hello Ethan
    # hello Michael
    

    alias_func_name 作为函数的引用,当然也可以作为函数被使用。

    2. 函数接受的参数和返回值都可以是函数

    def inc(x):
        return x + 1
    
    def dec(x):
        return x - 1
    
    def operate(func, x):
        result = func(x)
        return result
        
    operate(inc,3)
    # 4
    operate(dec,3)
    # 2
    

    这里 operate 中接受函数作为参数,并在其内部进行调用。

    3. 嵌套函数

    def increment():
        def inner_increment(number):
            return 1 + number
        return inner_increment()
    
    print(increment(100)) # 101
    

    在 increment 内部,实现对 number add 1 的操作。

    回头再来看下装饰器的实现:

    # def decorator
    def decorator_func(func):
        print('enter decorator..')
        def wrapper():
            print('Step1: enter wrapper func.')
            return func()
        return wrapper
    
    # def target func
    def normal_func():
        print("Step2: I'm a normal function.")
    
    # use decorator 
    normal_func = decorator_func(normal_func)
    normal_func()
    

    decorator_func(func) 中,参数 func 表示想要调用的函数,wrapper 为嵌套函数,作为装饰器的返回值。

    wrapper 内部会调用目标函数 func 并附加自己的行为,最后将 func 执行结果作为返回值。

    究其根本,是在目标函数外部套上了一层 wrapper 函数,达到在不改变原始函数本身的情况下,增加一些功能或者行为。

    通常使用时,使用 @decorator_func 来简化调用过程的两行代码。

    将自定义调用装饰器的两行代码删掉,使用常规装饰器的写法加在 normal_func 的定义处,但却不调用 normal_func,可以发现一个有趣的现象:

    # def decorator
    def decorator_func(func):
        print('enter decorator..')
        def wrapper():
            print('Step1: enter wrapper func.')
            return func()
        return wrapper
    
    # def target func
    @decorator_func
    def normal_func():
        print("Step2: I'm a normal function.")
    

    发现 enter decorator.. 在没有调用的情况下被打印到控制台。

    这就说明,此时 normal_func 已经变成了 wrapper 函数。

    @decorator_func 其实隐含了 normal_func = decorator_func(normal_func) 这一行代码。

    对带有参数的函数使用装饰器

    假设这里 normal_func 需要接受参数怎么办?

    很简单,由于是通过嵌套函数来调用目标函数,直接在 wrapper 中增加参数就可以了。

    # def decorator
    def decorator_func(func):
        def wrapper(*args, **kwargs):
            print('Step1: enter wrapper func.')
            return func(*args, **kwargs)
        return wrapper
    
    
    # def target func
    def normal_func(*args, **kwargs):
        print("Step2: I'm a normal function.")
        print(args)
        print(kwargs)
    
    # use decorator
    normal_func = decorator_func(normal_func)
    normal_func(1, 2, 3, name='zhang', sex='boy')
    

    使用 *args, **kwargs 是考虑到该 decorator 可以被多个不同的函数使用,而每个函数的参数可能不同。

    装饰器本身需要参数

    在装饰器本身也需要参数时,可以将其嵌套在另一个函数中,实现参数的传递。

    # def decorator
    def decorator_with_args(*args, **kwargs):
        print('Step1: enter wrapper with args func.')
        print(args)
        print(kwargs)
    
        def decorator_func(func):
            def wrapper(*args, **kwargs):
                print('Step2: enter wrapper func.')
                return func(*args, **kwargs)
            return wrapper
        return decorator_func
    
    
    # def target func
    def normal_func(*args, **kwargs):
        print("Step3: I'm a normal function.")
        print(args)
        print(kwargs)
    
    normal_func = decorator_with_args('first args')(normal_func)
    normal_func('hello')
    
    # use @ to replace the above three lines of code
    @decorator_with_args('first args')
    def normal_func(*args, **kwargs):
        print("Step3: I'm a normal function.")
        print(args)
        print(kwargs)
    

    来分析下 decorator_with_args 函数:

    • 由于 decorator_with_args 接受了任意数量的参数,同时由于 decorator_funcwrapper 作为其内部嵌套函数,自然可以访问其内部的作用域的变量。这样就实现了装饰器参数的自定义。
    • decorator_func 是正常的装饰器,对目标函数的行为进行包装。进而需要传递目标函数作为参数。

    在使用时:

    @decorator_with_args('first args') 实际上做的内容,就是 normal_func = decorator_with_args('first args')(normal_func) 的内容:

    1. decorator_with_args('first args') 返回 decorator_func 装饰器。
    2. decorator_func 接受的正常函数对象作为参数,返回包装的 wrapper 对象。
    3. 最后将 wrapper 函数重命名至原来的函数,使其在调用时保持一致。

    保留原函数信息

    在使用装饰器时,看起来原函数并没有被改变,但它的元信息却改变了 - 此时的原函数实际是包裹后的 wrapper 函数。

    help(normal_func)
    print(normal_func.__name__)
    
    # wrapper(*args, **kwargs)
    # wrapper
    

    如果想要保留原函数的元信息,可通过内置的 @functools.wraps(func) 实现:

    @functools.wraps(func) 的作用是通过 update_wrapperpartial 将目标函数的元信息拷贝至 wrapper 函数。

    # def decorator
    def decorator_with_args(*args, **kwargs):
        print('Step1: enter wrapper with args func.')
        print(args)
        print(kwargs)
    
        def decorator_func(func):
        	@functools.wraps(func)
            def wrapper(*args, **kwargs):
                print('Step2: enter wrapper func.')
                return func(*args, **kwargs)
            return wrapper
        return decorator_func
    

    装饰器嵌套

    Python 支持对一个函数同时增加多个装饰器,那么添加的顺序是怎样的呢?

    # def decorator
    def decorator_func_1(func):
        print('Step1: enter decorator_func_1..')
    
        def wrapper():
            print('Step2: enter wrapper1 func.')
            return func()
        return wrapper
    
    
    def decorator_func_2(func):
        print('Step1: enter decorator_func_2..')
    
        def wrapper():
            print('Step2: enter wrapper2 func.')
            return func()
        return wrapper
    
    @decorator_func_2
    @decorator_func_1
    def noraml_func():
        pass
    

    看一下 console 的结果:

    Step1: enter decorator_func_1..
    Step1: enter decorator_func_2..
    

    fun_1 在前说明, 在对原函数包装时,采用就近原则,从下到上。

    接着,调用 noraml_func 函数:

    Step1: enter decorator_func_1..
    Step1: enter decorator_func_2..
    Step2: enter wrapper2 func.
    Step2: enter wrapper1 func.
    

    可以发现,wrapper2 内容在前,说明在调用过程中由上到下。

    上面嵌套的写法,等价于 normal_func = decorator_func_2(decorator_func_1(normal_func)),就是正常函数的调用过程。

    对应执行顺序:

    1. 在定义时,先 decorator_func_1 后 decorator_func_2.
    2. 在调用时,先 decorator_func_2 后 decorator_func_1.

    应用场景

    日志记录

    在一些情况下,需要对函数执行的效率进行统计或者记录一些内容,但又不想改变函数本身的内容,这时装饰器是一个很好的手段。

    import timeit
    def timer(func):
        def wrapper(n):
            start = timeit.default_timer()
            result = func(n)
            stop = timeit.default_timer()
            print('Time: ', stop - start)
            return result
        return wrappe
    

    作为缓存

    装饰器另外很好的应用场景是充当缓存,如 lru 会将函数入参和返回值作为当缓存,以计算斐波那契数列为例, 当 n 值大小为 30,执行效率已经有很大差别。

    def fib(n):
        if n < 2:
            return 1
        else:
            return fib(n - 1) + fib(n - 2)
            
    @functools.lru_cache(128)
    def fib_cache(n):
        if n < 2:
            return 1
        else:
            return fib_cache(n - 1) + fib_cache(n - 2)
            
    Time:  0.2855725
    Time:  3.899999999995574e-05
    

    总结

    在这一篇中,我们知道:

    装饰器的本质,就是利用 Python 中的嵌套函数的特点,将目标函数包裹在内嵌函数中,然后将嵌套函数 wrapper作为返回值返回,从而达到修饰原函数的目的。

    而且由于返回的是 wrapper 函数,自然函数的元信息肯定不再是原函数的内容。

    对于一个函数被多个装饰器修饰的情况:

    • 在包装时,采用就近原则,从近点开始包装。
    • 在被调用时,采用就远原则,从远点开始执行。

    这自然也符合栈的调用过程。

    参考

    https://www.programiz.com/python-programming/decorator

  • 相关阅读:
    acme.sh 申请let's encrypt证书
    Excel 函数
    mysql索引失效的情况
    mysql之EXPLAIN优化分析
    mysql索引
    mysql视图
    mysql数据类型
    mysql约束
    mysql库和表的管理
    mysql的DML语言(增删改)
  • 原文地址:https://www.cnblogs.com/michael9/p/14721319.html
Copyright © 2011-2022 走看看