python函数装饰器的几个小技巧

给一个函数添加装饰器

给函数添加一个包装,以添加额外功能(比如计时、记录日志等等)

def outer(func):
	def inner(*args, **kwargs):
		print('{} is called'.format(func.__name__) )
		res = func(*args, **kwargs)
		print(res)
		return res
	return inner

@outer
def add(a, b):
	“”“this a add function”“”
	return a + b

add(1,2)

结果

add is called
3

如何保存被装饰函数的元数据(附底层原理,需要一定的功底)

上面我们定义了一个装饰器,并将它作用在add函数上,但是有一个问题,被装饰过add函数一些重要元数据被丢失了,比如函数名、函数文档等等

print(add.__name__)
print(add.__doc__)

结果

inner
None

这是因为被装饰过之后,add这个变量名已经指向装饰器的内部的inner函数对象了,所以才有上面的结果
如果想保存被装饰函数的元数据,需要为装饰器的内部函数也添加一个装饰器,它是位于python标准库的functools模块中的wraps函数

from functools import wraps


def outer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        print('{} is called'.format(func.__name__))
        res = func(*args, **kwargs)
        print(res)
        return res

    return inner


@outer
def add(a, b):
    """this is a add function"""
    return a + b


add(1, 2)

print('==============')
print(add.__name__)
print(add.__doc__)

结果

add is called
3
==============
add
this is a add function

加上这个@wraps装饰器,add函数的元数据就不会丢失了,@wraps装饰器的一个重要特性就是它可以让被装饰过的函数通过__wrapped__属性来访问被包装的自己,绕过装饰器,这个原理下面讲过wraps源码就会明白了,听着有点绕,那就先让我们直接看代码吧

print(add.__wrapped__(1, 2))

结果

3

你看根本没进入到我们定义的装饰器的inner里面去,是不是很神奇,接下来我们去看下functools.wraps源码,一探究竟,在探究wraps源码之前,我先说下,要想理解这个源码,需要知道functools.partial这个类的知识,
wraps源码以及涉及到的源码如下

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
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 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功底
@wraps装饰器的等价语法: inner = wraps(func)(inner)
wrap函数的文档的意思我翻译一下:这是一个装饰器工厂函数,它将update_wrapper() 这个函数绑定到包装函数上,wraps函数返回一个partial对象,这是一个偏函数对象,跟高等数学里偏导数概念有点类似,当我们调用add的时候,其实就是调用这个对象,partial实现了__call__方法,最终其实就是调用这个类的第一个参数update_wrapper函数,并将被装饰的函数传入以及固定的三个其他参数,在update_wrapper里完成了元数据的复制,注意还将被包装函数赋值给包装函数的__wrapped__属性,并返回包装函数,此时的包装函数inner已经具有add的元数据(__module__, __name__,__qualname____doc__, __annotations__),partial`源码如下

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

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

    def __new__(*args, **keywords):
        if not args:
            raise TypeError("descriptor '__new__' of partial needs an argument")
        if len(args) < 2:
            raise TypeError("type 'partial' takes at least one argument")
        cls, func, *args = args
        if not callable(func):
            raise TypeError("the first argument must be callable")
        args = tuple(args)

        if hasattr(func, "func"):
            args = func.args + args
            tmpkw = func.keywords.copy()
            tmpkw.update(keywords)
            keywords = tmpkw
            del tmpkw
            func = func.func

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

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

    def __call__(*args, **keywords):
        if not args:
            raise TypeError("descriptor '__call__' of partial needs an argument")
        self, *args = args
        newkeywords = self.keywords.copy()
        newkeywords.update(keywords)
        return self.func(*self.args, *args, **newkeywords)

    @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

核心在于__call__方法,好好理解下

如何对装饰器解包

如果装饰器的内部函数使用了@wraps装饰,那么就可以通过访问__wrapped__属性来获取对原始函数的访问,从而绕过装饰器,具体原理在上面一节已经说了。

猜你喜欢

转载自blog.csdn.net/weixin_42237702/article/details/91355521
今日推荐