python:functools.partial和functools.wraps使用

python:functools.partial和functools.wraps使用

1 前言

python内置的functools模块,提供了一些非常好用的类或者方法,其中functools.partial和functools.wraps的使用频率较高,本文将针对其分析使用。

2 使用

2.1 functools.partial

functools.partial是提供的工具类,python源码如下(本文参考自python3.9源码):

class partial:
    """New function with partial application of the given arguments
    and keywords.
    """

    __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"

    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(partial, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

    @recursive_repr()
    def __repr__(self):
        qualname = type(self).__qualname__
        args = [repr(self.func)]
        args.extend(repr(x) for x in self.args)
        args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
        if type(self).__module__ == "functools":
            return f"functools.{qualname}({', '.join(args)})"
        return f"{qualname}({', '.join(args)})"

    def __reduce__(self):
        return type(self), (self.func,), (self.func, self.args,
               self.keywords or None, self.__dict__ or None)

    def __setstate__(self, state):
        if not isinstance(state, tuple):
            raise TypeError("argument to __setstate__ must be a tuple")
        if len(state) != 4:
            raise TypeError(f"expected 4 items in state, got {len(state)}")
        func, args, kwds, namespace = state
        if (not callable(func) or not isinstance(args, tuple) or
           (kwds is not None and not isinstance(kwds, dict)) or
           (namespace is not None and not isinstance(namespace, dict))):
            raise TypeError("invalid partial state")

        args = tuple(args) # just in case it's a subclass
        if kwds is None:
            kwds = {}
        elif type(kwds) is not dict: # XXX does it need to be *exactly* dict?
            kwds = dict(kwds)
        if namespace is None:
            namespace = {}

        self.__dict__ = namespace
        self.func = func
        self.args = args
        self.keywords = kwds

根据源码,我们以同样的逻辑定义part类(功能类似partial):

def add(x, y):
    return x + y


class part:
    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(part, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):

        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)


print(hasattr(add, 'func'))
p = part(add, 10)
print(p(7))

效果和functools.partial类似,执行结果如下:

在这里插入图片描述

去掉__new__方法和__call__方法的“/,”和上述效果一致(去掉位置参数*args前加上的“/,”):

class part:
    def __new__(cls, func, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(part, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, *args, **keywords):

        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

执行结果不变:

在这里插入图片描述

但是注意,如果将“/,”替换为“_,”就会占据解构的位置参数,导致缺少参数而执行异常(python不像javascript,javascript可以使用空格加逗号(如[ , , a] = [1, 2, 3])来解构不需要的值,但是python一般是通过"_“来解构不需要的值,实际值是被”_"所占据了的(如 [ _, _, a] = [1, 2, 3])):

class part:
    def __new__(cls, _, func, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(part, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, _, *args, **keywords):

        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

执行报错(不能使用“_,”):

在这里插入图片描述

上面的part类,参考的functools.partial实现,核心是自定义实现__new__方法,达到自定义对象实例self的效果,同时为self添加属性func,即我们传入的函数,以及为self添加属性args和keywords,也就是上面我们的part(add, 10),除了第一个函数参数外,传入的固定的位置参数和关键字参数。同时第二个核心点是自定义实现__call__方法,__call__方法会使得对象成为callable类型的对象,也就是可以像函数一样调用,即func(),也就是当我们使用类名()来生成类的实例对象时,对实例对象再次调用实例对象()时,会调用__call__方法

__new__方法的了解可参考:python:__new__和__init__

下面是__call__方法的定义和使用示例:

class CallableClazz:
    def __call__(self, a, b):
        return a + b


func = CallableClazz()
result = func(4, 6)  # 实例对象,可以像函数一样调用,因为有实现__call__方法
print(result)
print(callable(func))
# 10
# True

由此可知,根据自定义实现__call__方法,解构关键字参数,即keywords = {**self.keywords, **keywords},self.keywords是我们固定的关键字参数,而keywords是后续调用函数对象,也就是上述调用p(7)中传入的关键字参数;同理,self.func(*self.args, args, **keywords)中传入的self.args, *args,self.args也是固定的位置参数,args是调用p(7)中传入的位置参数,这里是7,所以可以达到固定参数值的情况下,执行函数。

小结:partial通过实现"__new__“和”__call__"生成一个可调用对象,这个对象内部保存了被包装函数以及固定参数,这个对象可以像函数一样被调用,调用时,其实是执行了对象内部持有的被包装函数,其参数由固定参数和新传入的参数组合而来。

2.2 functools.wraps

有了上述functools.partial的源码分析,再参考python源码functools.wraps如下:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

这里我们需要提前知道一个知识点,也就是python的装饰器中,def的函数中返回inner函数,同时def定义的函数的接收参数亦是函数,就可以达到装饰器的效果。而若inner函数是callable的,也就是类似类实现了__call__方法,那么也可以作为类似inner函数的效果,形成python装饰器

由此可知,上述的wraps函数,实际是python的装饰器,因为返回的是callable的类实例partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated),该类实例还可以执行:类实例(),也就是实现了__call__方法,可以类似函数调用,所以亦可充当装饰器

那么接下来分析其效果:

返回的partial实例对象,第一个参数是update_wrapper,其为函数,python functools源码如下:

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

而partial实例对象的第二个参数是wrapped,也就是装饰器修饰的原本函数对象;第三个参数是assigned,如下源码所示:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')

partial实例对象的第四个参数是updated,源码如下:

WRAPPER_UPDATES = ('__dict__',)

所以实际返回的partial实例对象,是对方法update_wrapper,固定了位置参数

根据functools工具类的源码,我们实现下述的功能并分析效果:

class part:
    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(part, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):

        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)


WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)


def wrapsNew(wrapped,
             assigned=WRAPPER_ASSIGNMENTS,
             updated=WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return part(update_wrapper, wrapped=wrapped,
                assigned=assigned, updated=updated)


def update_wrapper(wrapper,
                   wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper


def outer(func):
    @wrapsNew(func)
    def inner(*args, **kwargs):
        print(f"before...")
        func(*args, **kwargs)
        print("after...")

    return inner


@outer
def add(a, b):
    """
    求和运算
    """
    print(a + b)

对于add方法,我们执行如下:

if __name__ == '__main__':
    print(add(1, 6))

执行前分析如下:

对于代码:

@outer
def add(a, b):
    """
    求和运算
    """
    print(a + b)

根据装饰器语法糖的形式,实际是如下的效果:

add = outer(add)   
意即:相当于于我们调用的add方法,是outer(add)方法,即outer(add)(1, 6)

outer方法如下:

def outer(func):
    @wrapsNew(func)
    def inner(*args, **kwargs):
        print(f"before...")
        func(*args, **kwargs)
        print("after...")

    return inner

那么继续分析:

而outer(add) 方法的返回值,相当于: inner = wrapsNew(add)(inner)  return inner
即: outer(add) = wrapsNew(add)(inner)
即: outer(add) = part(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)(inner)
即: add = part(update_wrapper, wrapped=add方法,
                   assigned=assigned, updated=updated)(inner)

上述拆解分析后可知:

  • wrapped是 add 方法,也就是我们原生定义的add方法,不加任何修饰改变的
  • assigned是 WRAPPER_ASSIGNMENTS = (‘__module__’, ‘__name__’,‘__qualname__’, ‘__doc__’, ‘__annotations__’)
  • updated是 WRAPPER_UPDATES = (‘__dict__’,)

也就是说,add经装饰后的新方法,是通过我们自定义的part类绑定了3个关键字参数(kwargs),然后传入参数outer.inner并调用得来的,关键字参数如下:

  • wrapped(add定义的原方法);
  • assigned,即(‘__module__’, ‘__name__’, ‘__qualname__’,‘__doc__’, ‘__annotations__’);
  • updated,即(‘__dict__’,);

所以下述执行__new__方法时,func是update_wrapper方法,而self.args为空元组,self.keywords是关键字参数字典dict,值也就是上述提到的绑定的3个关键字参数:

class part:
    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(part, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords

然后执行part(…)(inner)的时候,就会调用如下part类的call方法:

def __call__(self, /, *args, **keywords):

    keywords = {**self.keywords, **keywords}
    return self.func(*self.args, *args, **keywords)

debug结果如下可见,keywords就是上述的3个关键字参数:

在这里插入图片描述

args是调用的part(…)(inner)传入的inner方法:

在这里插入图片描述

而self.args因为调用part类时没有传入需要固定的位置参数,所以是空元组:

在这里插入图片描述

也就是说,上述的调用,最终表现形式如下:

update_wrapper(
inner方法(位置参数), 
{wrapped: add原方法, 
assigned: ('\_\_module\_\_', '\_\_name\_\_', '\_\_qualname\_\_','\_\_doc\_\_', '\_\_annotations\_\_'), 
updated: ('\_\_dict\_\_',)}(关键字参数))

故而如下参数中,wrapper是inner方法,wrapped是add原方法:

def update_wrapper(wrapper,
                   wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):

所以核心逻辑是update_wrapper方法:

def update_wrapper(wrapper,
                   wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

debug可知,下述逻辑,是把wrapped原方法(也就是@wrapsNew装饰器的方法参数,这里是add方法)的一些assigned属性获取到(即’__module__‘, ‘__name__’, ‘__qualname__’,’__doc__', ‘__annotations__’),然后将结果赋值给wrapper方法(也就是@wrapsNew装饰的方法inner):

__module__属性:

在这里插入图片描述

__name__属性:

在这里插入图片描述

__qualname__属性:

在这里插入图片描述

__doc__属性:

在这里插入图片描述

也就是add方法的doc:

在这里插入图片描述

__annotations__属性:

在这里插入图片描述

然后针对@wrapsNew装饰的方法inner的__dict__属性进行更新:

inner的__dict__属性:

在这里插入图片描述

add的__dict__属性:

在这里插入图片描述

for attr in updated:
    getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

将inner方法的属性更新部分为wrapped方法的属性后,最后将wrapper(inner)方法的__wrapped__属性设置为wrapped(add)方法:

# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped

最后返回wrapper(inner)方法:

# Return the wrapper so this can be used as a decorator via partial()
return wrapper

到此全部分析已完成接下来就是执行如下逻辑,func就是wrapped方法,add:

def add(a, b):
    """
    求和运算
    """
    print(a + b)
def inner(*args, **kwargs):
    print(f"before...")
    func(*args, **kwargs)
    print("after...")

最终执行结果如下:

在这里插入图片描述

上述可知,原方法我们也可以获取并执行,通过装饰后方法.__wrapped__获取到原方法并执行:

print(add.__wrapped__(2, 8))

结果:

10
None

同时,装饰后的add方法的一些属性,也被更新为原方法add的属性,这样更加贴合原有方法的语义:

print(add.__module__)
print(add.__name__)
print(add.__qualname__)
print(add.__doc__)
print(add.__annotations__)
print(add.__dict__)

结果如下:

在这里插入图片描述

若为add方法增加annotation信息(注意:python的annotation仅提示作用,不是强制类型限制,且和java的注解Annotation是完全不同,反编译可知java的注解Annotation是继承了Annotation接口的特殊接口),如下为add原方法的参数和返回值添加注解annotation:

@outer
def add(a: int, b: int) -> int:
    """
    求和运算
    """
    print(a + b)
    return a + b

执行如下:

在这里插入图片描述

结果如下,可见inner方法的annotation注解信息(inner方法实际是没有注解信息的),实际是使用的add方法的注解元信息:

在这里插入图片描述

获取的注解信息如下:

在这里插入图片描述

结果:

在这里插入图片描述

或者上述的annotation注解信息获取,可以使用inspect模块中的Signature对象,对可调用对象进行内省

在这里插入图片描述

import inspect

signature = inspect.signature(add)
print(signature.parameters["a"])
print(signature.parameters["a"].annotation)
print(signature.parameters["a"].annotation == int)
print(signature.parameters["a"].annotation is int)

结果和上述类似:

在这里插入图片描述

再针对多个装饰器举个栗子

import functools


def decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('decorator 1 begin.')
        return func(*args, **kwargs)

    return wrapper


def decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('decorator 2 begin.')
        return func(*args, **kwargs)

    return wrapper


@decorator1
@decorator2
def add(x: int, y: int) -> int:
    print("开始累加:")
    print(x + y)


if __name__ == '__main__':
    print("1**********\n")
    add(1, 2)
    print("2**********\n")
    add.__wrapped__(1, 2)
    print("3**********\n")
    add.__wrapped__.__wrapped__(1, 2)

执行结果如下:

在这里插入图片描述

注意分析如下代码:

@decorator1
@decorator2
def add(x: int, y: int) -> int:
    print("开始累加:")
    print(x + y)

其本质如下:

decorator1 = decorator1(add1)

注意这里的add1是下面的add2,而add2其实是decorator2(add):

add2 = decorator2(add)

所以公式如下:

add = decorator1(decorator2(add))

所以执行完,一定是decorator1先打印,再是decorator2打印。同时基于此,由上述我们可知,add可以多次调用__wrapped__方法。

add方法调用一次__wrapped__时,实际返回的是decorator1的包裹原方法,所以原方法是decorator2(add),故而执行可见,打印了:decorator 2 begin.语句。

而add方法连续调用两次__wrapped__时,实际返回的decorator2(add)的包裹原方法,也就是我们自定义的原方法add,故而decorator 1 begin.和decorator 2 begin.语句均未打印。

小结:

1)functools.wraps 旨在消除装饰器对原函数造成的影响,即对原函数的相关属性进行拷贝,以达到装饰器不修改原函数的目的(保存函数的元数据,更加符合原方法语义、属性)。

2)wraps内部通过partial对象和update_wrapper函数实现。

3)partial是一个类,核心通过实现__new__和__call__方法,自定义实例化对象过程,使得对象内部保留原函数和固定参数,__call__方法使得对象可以像函数一样被调用,再通过内部保留的原函数和固定参数以及传入的其它参数进行原函数调用,wraps函数实现返回partial类实例,使得其可视作装饰器使用。

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值