简体   繁体   English

保留装饰函数的签名

[英]Preserving signatures of decorated functions

Suppose I have written a decorator that does something very generic.假设我写了一个装饰器来做一些非常通用的事情。 For example, it might convert all arguments to a specific type, perform logging, implement memoization, etc.例如,它可能将所有参数转换为特定类型、执行日志记录、实现记忆等。

Here is an example:下面是一个例子:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

Everything well so far.到目前为止一切都很好。 There is one problem, however.然而,有一个问题。 The decorated function does not retain the documentation of the original function:修饰函数不保留原函数的文档:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

Fortunately, there is a workaround:幸运的是,有一个解决方法:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

This time, the function name and documentation are correct:这一次,函数名称和文档是正确的:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

But there is still a problem: the function signature is wrong.但是还有一个问题:函数签名是错误的。 The information "*args, **kwargs" is next to useless.信息 "*args, **kwargs" 几乎没用。

What to do?该怎么办? I can think of two simple but flawed workarounds:我可以想到两个简单但有缺陷的解决方法:

1 -- Include the correct signature in the docstring: 1 -- 在文档字符串中包含正确的签名:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

This is bad because of the duplication.由于重复,这很糟糕。 The signature will still not be shown properly in automatically generated documentation.签名仍不会在自动生成的文档中正确显示。 It's easy to update the function and forget about changing the docstring, or to make a typo.很容易更新函数而忘记更改文档字符串或输入错误。 [ And yes, I'm aware of the fact that the docstring already duplicates the function body. [是的,我知道文档字符串已经复制了函数体。 Please ignore this;请忽略这一点; funny_function is just a random example.有趣的功能只是一个随机的例子。 ] ]

2 -- Not use a decorator, or use a special-purpose decorator for every specific signature: 2 -- 不使用装饰器,或为每个特定签名使用专用装饰器:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

This works fine for a set of functions that have identical signature, but it's useless in general.这适用于一组具有相同签名的函数,但一般来说是无用的。 As I said in the beginning, I want to be able to use decorators entirely generically.正如我在开头所说的,我希望能够完全通用地使用装饰器。

I'm looking for a solution that is fully general, and automatic.我正在寻找一种完全通用且自动的解决方案。

So the question is: is there a way to edit the decorated function signature after it has been created?所以问题是:有没有办法在创建后编辑装饰函数签名?

Otherwise, can I write a decorator that extracts the function signature and uses that information instead of "*kwargs, **kwargs" when constructing the decorated function?否则,我是否可以编写一个装饰器来提取函数签名并在构造装饰函数时使用该信息而不是“*kwargs, **kwargs”? How do I extract that information?我如何提取该信息? How should I construct the decorated function -- with exec?我应该如何使用 exec 构造装饰函数?

Any other approaches?还有其他方法吗?

  1. Install decorator module: 安装装饰模块:

     $ pip install decorator 
  2. Adapt definition of args_as_ints() : 修改args_as_ints()定义:

     import decorator @decorator.decorator def args_as_ints(f, *args, **kwargs): args = [int(x) for x in args] kwargs = dict((k, int(v)) for k, v in kwargs.items()) return f(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x*y + 2*z print funny_function("3", 4.0, z="5") # 22 help(funny_function) # Help on function funny_function in module __main__: # # funny_function(x, y, z=3) # Computes x*y + 2*z 

Python 3.4+ Python 3.4+

functools.wraps() from stdlib preserves signatures since Python 3.4: 来自stdlib的functools.wraps()保留了自Python 3.4以来的签名:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps() is available at least since Python 2.5 but it does not preserve the signature there: functools.wraps() 至少从Python 2.5开始可用但它不保留那里的签名:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

Notice: *args, **kwargs instead of x, y, z=3 . 注意: *args, **kwargs而不是x, y, z=3

This is solved with Python's standard library functools and specifically functools.wraps function, which is designed to " update a wrapper function to look like the wrapped function ". 这是通过Python的标准库functools ,特别是functools.wraps函数解决的,该函数旨在“ 将包装函数更新为包装函数 ”。 It's behaviour depends on Python version, however, as shown below. 但是,它的行为取决于Python版本,如下所示。 Applied to the example from the question, the code would look like: 应用于问题的示例,代码如下所示:

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

When executed in Python 3, this would produce the following: 在Python 3中执行时,会产生以下结果:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

Its only drawback is that in Python 2 however, it doesn't update function's argument list. 它唯一的缺点是在Python 2中,它不会更新函数的参数列表。 When executed in Python 2, it will produce: 在Python 2中执行时,它将产生:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

There is a decorator module with decorator decorator you can use: 有一个装饰器模块 ,你可以使用decoratordecorator器:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

Then the signature and help of the method is preserved: 然后保留方法的签名和帮助:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

EDIT: JF Sebastian pointed out that I didn't modify args_as_ints function -- it is fixed now. 编辑:JF塞巴斯蒂安指出我没有修改args_as_ints函数 - 它现在已修复。

看看装饰模块 - 特别是装饰装饰器,它解决了这个问题。

Second option: 第二种选择:

  1. Install wrapt module: 安装包装模块:

$ easy_install wrapt $ easy_install包装

wrapt have a bonus, preserve class signature. 保护有奖金,保留类签名。


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z

As commented above in jfs's answer ; 正如上面在jfs的回答中所评论的那样 ; if you're concerned with signature in terms of appearance ( help , and inspect.signature ), then using functools.wraps is perfectly fine. 如果您关注外观( helpinspect.signature )方面的签名,那么使用functools.wraps就可以了。

If you're concerned with signature in terms of behavior (in particular TypeError in case of arguments mismatch), functools.wraps does not preserve it. 如果您关注行为方面的签名(特别是参数不匹配时的TypeError ), functools.wraps不会保留它。 You should rather use decorator for that, or my generalization of its core engine, named makefun . 您应该使用decorator ,或者我的核心引擎,名为makefun

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

See also this post about functools.wraps . 另见这篇关于functools.wraps

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

this fixes name and documentation.这修复了名称和文档。 to preserve the function signature, wrap is used exactly at same location as g.__name__ = f.__name__, g.__doc__ = f.__doc__ .为了保留函数签名,在与g.__name__ = f.__name__, g.__doc__ = f.__doc__完全相同的位置使用wrap

the wraps itself a decorator. wraps本身就是一个装饰器。 we pass the closure-the inner function to that decorator, and it is going to fix up the metadata.我们将闭包 - 内部函数传递给该装饰器,它将修复元数据。 BUt if we only pass in the inner function to wraps , it is not gonna know where to copy the metadata from.但是如果我们只将内部函数传递给wraps ,它就不会知道从哪里复制元数据。 It needs to know which function's metadata needs to be protected.它需要知道需要保护哪个函数的元数据。 It needs to know the original function.它需要知道原始函数。

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g=wraps(f)(g)
    return g

wraps(f) is going to return a function which will take g as its parameter. wraps(f)将返回一个将g作为其参数的函数。 And that is going to return closure and will assigned to g and then we return it.这将返回闭包并将分配给g然后我们返回它。

from inspect import signature


def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    sig = signature(f)
    g.__signature__ = sig
    g.__doc__ = f.__doc__
    g.__annotations__ = f.__annotations__
    g.__name__ = f.__name__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

I wanted to add that answer (since this shows up first in google).我想添加那个答案(因为它首先出现在谷歌中)。 The inspect module is able to fetch the signature of a function, so that it can be preserved in decorators.检查模块能够获取函数的签名,以便它可以保存在装饰器中。 But that's not all.但这还不是全部。 If you want to modify the signature, you can do so like this :如果你想修改签名,你可以这样做:

from inspect import signature, Parameter, _ParameterKind


def foo(a: int, b: int) -> int:
    return a + b

sig = signature(foo)
sig._parameters = dict(sig.parameters)
sig.parameters['c'] = Parameter(
    'c', _ParameterKind.POSITIONAL_OR_KEYWORD, 
    annotation=int
)
foo.__signature__ = sig

>>> help(foo)
Help on function foo in module __main__:

foo(a: int, b: int, c: int) -> int

Why would you want to mutate a function's signature ?为什么要改变函数的签名?

It's mostly useful to have adequate documentation on your functions and methods.对您的函数和方法有足够的文档是最有用的。 If you're using the *args, **kwargs syntax and then popping arguments from kwargs for other uses in your decorators, that keyword argument won't be properly documented, hence, modifying the signature of the function.如果您使用*args, **kwargs语法,然后从 kwargs 中弹出参数用于装饰器中的其他用途,则该关键字参数将不会被正确记录,因此需要修改函数的签名。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM