Python装饰器进阶之二

释放双眼,带上耳机,听听看~!

Python装饰器进阶之二

保存被装饰方法的元数据

什么是方法的元数据

举个栗子

def hello():
    print('Hello, World.')

print(dir(hello))

结果如下:

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

其中:

__name__: 代表方法的名字
__doc__: 代表方法的字符串文档(实际上就是"""..."""这种形式的注释)
__moudle__: 方法所属模块的名字
__dict__: 属性字典(这个属性在面向对象编程时很重要,用好了能大大节约Python内存的开支)
__defaults__: 方法参数中,默认参数的值(实际上Python方法的默认参数是创建方法对象的时候就存储在这里的)
...
等等

以下面一个为例:

def hello(numa, numb=1, numc=[]):
    """
        Print numa, numb, numc.
    """
    print(numa, numb, numc)
    return True

print(hello.__name__)
print(hello.__doc__)
print(hello.__module__)
print(hello.__dict__)
print(hello.__defaults__)

结果如下:

hello

        Print numa, numb, numc.

__main__
{}
(1, [])
[Finished in 0.1s]

我们可以看到,__doc__实际上就是,方法里面用三引号包裹起来的注释。而__dict__则是方法属性的字典,我们这个方法对象并没有任何的属性,所以说他是空的。

我们给方法增加一个属性:

def hello():
    print('Hello, World.')

hello.name = 'XiaoMing'
print(hello.__dict__)

结果如下:

{'name': 'XiaoMing'}

甚至我们还可以这样:

def hello():
    print('Hello, World.')

hello.__dict__['name'] = 'XiaoMing'
print(hello.name)

结果如下:

XiaoMing

同样的,我们的__defaults__属性本身是一个元组,元组是不可变类型的数据结构。但是现在我们的numc使用的是一个列表,那我们是不是可以改变方法默认参数的值呢:

def hello(numa, numb=1, numc=[]):
    print(numa, numb, numc)

# 一共两个元素,下标1的元素就代表了我们的numc所对应的列表
hello.__defaults__[1].append('Hello')
hello(100)

结果如下:

100 1 ['Hello']

所以,在我们方法的默认参数上面,应该避免使用数组这种可变类型的数据结构。因为Python本身把__defaults__属性设置为元组,那就是希望人们无法去修改它,如果我们使用了可变类型的数据结构就违背了Python的本意。

说了这么多废话,没有进入主题,来看装饰器对方法元数据的影响,上一章的例子:

def add_cache(func):
    """
        This add_cache
    """
    cache = {}
    def wrap(*args):
        """
            This wrap
        """
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrap

@add_cache
def fibonacci(n):
    """
        This fibonacci
    """
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

# 实际上返回的对象是wrap方法的对象,所以得到的也是wrap方法的元数据
print(fibonacci.__name__)
print(fibonacci.__doc__)

结果如下:

wrap

            This wrap

如何保存被装饰方法的元数据不被改变

这样就存在一个问题,我们的方法被装饰以后,原来的某些东西,我们无法访问了,这肯定是不行的,那我们必须想办法能够在装饰以后还保持某些元数据是原来方法的元数据。

简单思考以后我们可以这样做:

def add_cache(func):
    """
        This add_cache
    """
    cache = {}
    def wrap(*args):
        """
            This wrap
        """
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    # 返回之前,我们修改这个对象的元数据让它等于原方法的元数据
    wrap.__name__ = func.__name__
    wrap.__doc__ = func.__doc__
    return wrap

@add_cache
def fibonacci(n):
    """
        This fibonacci
    """
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci.__name__)
print(fibonacci.__doc__)

结果和我们设想的一样:

fibonacci

        This fibonacci

虽然是实现了我们的目的,但是这么做非常的不优雅,有没有比较优雅的做法呢。

我们可以使用Python标准库functools下的update_wrapper来实现:

from functools import update_wrapper

def add_cache(func):
    """
        This add_cache
    """
    cache = {}
    def wrap(*args):
        """
            This wrap
        """
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    # 使用update_wrapper来进行替换
    update_wrapper(wrap, func, assigned=('__name__',), updated=('__dict__',))
    return wrap

@add_cache
def fibonacci(n):
    """
        This fibonacci
    """
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci.__name__)
print(fibonacci.__doc__)

结果如下:

fibonacci

            This wrap

解析:

update_wrapper:
    第一个参数:代表装饰方法
    第二个参数:代表被装饰方法
    assigned:代表那些属性是需要替换的,不写的就代表不替换。(可以省略不写assigned=)
    updated:代表哪些属性需要合并,因为原方法有一些属性,装饰方法也有一些属性,所以他们两个里面的内容,需要合并在一起。(同样可以省略不写updated=)

需要注意的是呢,update_wrapper中的assigned和updated都有一个默认的参数,来看一下这个方法的源代码:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
所以,即使我们不指定后两个参数,也是可以实现我们的需求的。

还有一种更方便的做法,Python为我们提供了一个装饰器:@wraps(),同样在functools下面,这个装饰器是用在装饰方法上面的,它接收三个参数,分别是被装饰方法,assigned和updated。当然,后两个参数都有默认值,同样是WRAPPER_ASSIGNMENTS和WRAPPER_UPDATES,所以我们可以这样:

from functools import wraps

def add_cache(func):
    """
        This add_cache
    """
    cache = {}
    # 使用装饰器来保存被装饰方法的元数据
    @wraps(func)
    def wrap(*args):
        """
            This wrap
        """
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrap

@add_cache
def fibonacci(n):
    """
        This fibonacci
    """
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci.__name__)
print(fibonacci.__doc__)

结果如下:

fibonacci

        This fibonacci

实际上在@wraps()这个装饰器内部,使用的就是update_wrapper()方法。

END

【转自慕课】https://www.imooc.com

Python

python强势来袭-入门0001

2022-3-3 5:37:39

Python

初识 Python:全局、局部和非局部变量(带示例)

2022-3-3 5:40:57

搜索