Skip to content

Latest commit

 

History

History
275 lines (211 loc) · 8.07 KB

functional_programming11.md

File metadata and controls

275 lines (211 loc) · 8.07 KB

Python函数式——还在装饰?

装饰器是Python一个重要且极具Python特点的特性,所以本期我们继续带来装饰器相关的内容。

保留签名

我们在最早介绍装饰器的时候(在这里)提到过怎样详细保留被装饰函数的签名,这里详细介绍一下。

对于如下一个普通的装饰器:

def decorator(func):
    def wrapper(*args, **kwargs):
        print('This is wrapper function')
        return func(*args, **kwargs)
    return wrapper
  
@decorator
def func(a):
    '''Docstring of function func

    Args:
        a (any): first parameter
    Returns:
        any: a
    '''
    print(f'This is original function with {a}')
    return a
    
func(1)
# This is wrapper function
# This is original function with 1

我们知道,装饰器写法等价于:

func = decorator(func) 
# decorator返回一个wrapper函数,标识符func指向了这个函数对象

但是,经过装饰的函数,其元数据(参数列表,docstring等)变成什么了呢?如果我们去掉@decorator

help(func)
# Help on function func in module __main__:
# 
# func(a)
#     Docstring of function func
# 
#     Args:
#         a (any): first parameter
#     Returns:
#         any: a

from inspect import signature
print(signature(func))
# (a)

而加上装饰之后再运行:

help(func)
# Help on function wrapper in module __main__:
# 
# wrapper(*args, **kwargs)
print(signature(func))
# (*args, **kwargs)

这是因为,func标识符指向了decorator所返回的函数wrapper上了,所以helpsignature查看的是wrapper函数的信息。这样的装饰器虽然功能上没有问题,但是其他使用者无法获知函数的使用方式。如果希望在装饰之后还可以保留被装饰函数的元数据,需要使用functools标准库下的update_wrapper方法:

from functools import update_wrapper

def decorator(func):
    def wrapper(*args, **kwargs):
        print('This is wrapper function')
        return func(*args, **kwargs)
    update_wrapper(wrapper, func)
    return wrapper

@decorator
def func(a):
    '''Docstring of function func'''
    print(f'This is original function with {a}')
    return a

help(func)
# Help on function func in module __main__:
# 
# func(a)
#     Docstring of function func
print(signature(func))
# (a)

update_wrapper实现方式是将被装饰函数的元信息(__doc__, __name__等)直接替换进装饰函数中。update_wrapper也有一种替代写法,即利用functools.wraps装饰器:

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('This is wrapper function')
        return func(*args, **kwargs)
    return wrapper

@decorator
def func(a):
    '''Docstring of function func'''
    print(f'This is original function with {a}')
    return a

help(func)
# Help on function func in module __main__:
# 
# func(a)
#     Docstring of function func
print(signature(func))
# (a)

@wraps为装饰器增加了一个属性__wrapped__,其内容即为被装饰的函数:

print(func.__wrapped__.__doc__)
# Docstring of function func

需要注意的是,在Python 3.4版本以前,__wrapped__并非一定指向的是被装饰的函数,这是因为某些装饰器可能自身就定义了__wrapped__属性,把被装饰函数覆盖掉了(例如@lru_cache)。幸运的是,这一个bug在Python 3.4版本被修复。结论是,在Python中,只要编写装饰器,就应当采用@wraps

保持函数参数一致

在编写装饰器的过程中,一个比较常见的问题是装饰函数与被装饰函数的参数列表是可以不一致的:

from functools import wraps
def decorator(func):
    @wraps(func)
    def wrapper(a, b, c): # 这里可以随意定义
        return func(a, b)
    return wrapper
  
@decorator
def func(a, b): # 这里也可以随意定义
    print(a, b)

这里,funcwrapper参数列表是不一致的,所以用户只能按照wrapper的参数列表去调用func,但是用户从func的帮助信息中只能看到a, b两个参数,这就导致了不一致的问题。当然,我们可以将wrapper定义为*args**kwargs,这样,只要使用者按照函数的文档来调用函数,就不会出问题:

from functools import wraps
def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
  
@decorator
def func(a, b):
    print(a, b)
    
func(1, 2)
# 1 2
func(1)
# TypeError: func() missing 1 required positional argument: 'b'

这样的方式也存在一定的问题,也就是异常抛出仅发生在真正调用被装饰函数的时候,所有位于调用之前的程序都会被执行。通常,我们更希望在装饰函数调用时刻就抛出参数不符的异常,这也符合普通函数的执行过程。要实现这一点,我们需要将被装饰函数的参数列表绑定到装饰函数的参数列表上:

from functools import wraps
from inspect import signature, Signature

def decorator(func):
    func_sig = signature(func)
    sig = Signature(func_sig.parameters.values())
    @wraps(func)
    def wrapper(*args, **kwargs):
        bound_sig = sig.bind(*args, **kwargs)
        print('This executes before func')
        return func(*bound_sig.args, **bound_sig.kwargs)
    return wrapper

@decorator
def func(a, b, c=True):
    print(a, b, c)
    
func(1, 2, 3)
# This executes before func
# 1 2 3

func(1, 2)
# This executes before func
# 1 2 True

func(a='a', b='b')
# This executes before func
# a b True

func(a='a', b='b', c='c', d=4)
# TypeError: got an unexpected keyword argument 'd'

func(1)
# TypeError: missing a required argument: 'b'

decorator中,我们首先利用signature获取了func的函数签名(即参数列表),然后构建了一个Signature对象。Signature对象只能利用一个具有Parameter对象的元组来初始化,而一个Parameter对象表示函数的一个参数。所以我们最终获得的sig即函数func的签名对象。在wrapper中,我们将sig绑定到可变参数*args**kwargs上,这样,如果可变参数列表同sig不一致时,就会抛出TypeError异常。

可选参数装饰器

所谓可选参数,即装饰器可以选择带有参数,也可以不带参数直接装饰,例如:

@decorator
def func(): pass

或者:

@decorator(param=1)
def func(): pass

两者的实现方式是不同的,如果希望装饰器能够接收参数,那么需要两层函数的嵌套,而普通的装饰器仅需要嵌套一层函数定义。这里我们尝试将两种模式集中在一起,从而实现程序的一致性。需要指出的是,额外的参数只能以关键字参数方式提供:

from functools import partial, wraps

def decorator(func=None, *, param=1, param2=True):
    if func is None:
        return partial(decorator, param=param, param2=param2)

    @wraps(func)
    def wrapper(*args, **kwargs):
        print(param, param2)
        return func(*args, **kwargs)
    return wrapper

@decorator
def func1(a, b=1):
    print(a, b)

@decorator(param=2, param2=False)
def func2(a, b=2):
    return a, b

func1(1)
# param: 1 param2: True
# 1 1

print(func2(2))
# param: 2 param2: False
# (2, 2)

在示例中,decorator的两种装饰方法,分别可以拆成:

func1 = decorator(func1)
func2 = decorator(param=2, param2=False)(func2)

func1和普通的装饰器没有区别,我们来看一下func2的装饰流程。首先,decoratorfuncNone,所以会进入if中,并利用偏函数partial将已经接收的参数paramparam2绑定到了decorator中,并将新版本的decorator再次返回,亦即:

func2 = decorator(param=2, param2=False)(func2)
      = decorator(func2, param=2, param2=False)

为什么要加*?因为后边的参数必须是关键字参数,否则,第一个位置参数会被decorator认为是func而导致错误。