保留装饰函数的签名

2025-02-10 08:57:00
admin
原创
52
摘要:问题描述:假设我编写了一个装饰器,它的功能非常通用。例如,它可能将所有参数转换为特定类型、执行日志记录、实现记忆等。以下是一个例子:def args_as_ints(f): def g(*args, **kwargs): args = [int(x) for x in args] ...

问题描述:

假设我编写了一个装饰器,它的功能非常通用。例如,它可能将所有参数转换为特定类型、执行日志记录、实现记忆等。

以下是一个例子:

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

到目前为止一切都很好。但是有一个问题。修饰函数不保留原始函数的文档:

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

g(*args, **kwargs)

幸运的是,有一个解决方法:

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

这次,函数名称和文档是正确的:

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

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

但还有一个问题:函数签名是错误的。“args, *kwargs”信息几乎毫无用处。

该怎么办?我能想到两个简单但有缺陷的解决方法:

1——在文档字符串中包含正确的签名:

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

由于重复,这很糟糕。签名仍然无法在自动生成的文档中正确显示。很容易更新函数而忘记更改文档字符串,或者输入错误。[是的,我知道文档字符串已经重复了函数主体。请忽略这一点;funny_function 只是一个随机示例。 ]

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

对于具有相同签名的一组函数来说,这很好用,但总的来说没用。正如我在开头所说的,我希望能够完全通用地使用装饰器。

我正在寻找一个完全通用且自动化的解决方案。

所以问题是:在创建装饰函数签名之后,有没有办法编辑它?

否则,我可以编写一个装饰器来提取函数签名并在构造装饰函数时使用该信息而不是“kwargs,*kwargs”吗?我如何提取该信息?我应该如何使用 exec 构造装饰函数?

还有其他方法吗?


解决方案 1:

  1. 安装装饰器模块:

$ pip install decorator
  1. 调整定义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+

functools.wraps()从 Python 3.4 开始, stdlib保留了签名:

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()至少从 Python 2.5 开始可用,但它没有保留签名:

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

注意:*args, **kwargs而不是x, y, z=3

解决方案 2:

functools这个问题可以通过 Python 的标准库和函数来解决functools.wraps,该函数旨在“更新包装函数,使其看起来像包装函数”。但它的行为取决于 Python 版本,如下所示。应用到问题中的示例,代码将如下所示:

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

在 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

它唯一的缺点是,在 Python 2 中,它不会更新函数的参数列表。在 Python 2 中执行时,它将产生:

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

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

解决方案 3:

有一个带有装饰器的装饰器模块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)

然后保留方法的签名和帮助:

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

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

编辑:JF Sebastian 指出我没有修改args_as_ints功能——现在已经修复。

解决方案 4:

看一下装饰器模块——特别是装饰器decorator,它解决了这个问题。

解决方案 5:

第二种选择:

  1. 安装 wrapt 模块:

$ easy_install wrapt

wrapt 有奖金,保留班级签名。


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

解决方案 6:

正如上面jfs 的回答中所说;如果您关心外观方面的签名(help,和inspect.signature),那么使用functools.wraps就完全没问题。

如果您关心签名的行为(特别是TypeError在参数不匹配的情况下),functools.wraps则不保留它。您应该使用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)

另请参阅有关此帖子functools.wraps

解决方案 7:

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

我想添加这个答案(因为这首先出现在谷歌中)。inspect 模块能够获取函数的签名,以便可以将其保存在装饰器中。但这还不是全部。如果您想修改签名,可以这样做:

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

为什么要改变函数的签名?

对函数和方法进行充分的文档记录非常有用。如果您使用该*args, **kwargs语法,然后从 kwargs 中弹出参数以用于装饰器中的其他用途,则该关键字参数将无法正确记录,因此会修改函数的签名。

解决方案 8:

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

这修复了名称和文档。为了保留函数签名,wrap与在完全相同的位置使用g.__name__ = f.__name__, g.__doc__ = f.__doc__

本身wraps就是一个装饰器。我们将闭包(内部函数)传递给该装饰器,它将修复元数据。但是,如果我们只将内部函数传递给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=wraps(f)(g)
    return g

wraps(f)将返回一个函数,该函数将接受g作为其参数。它将返回闭包,并将其赋值给g,然后我们将其返回。

解决方案 9:

正如已经回答的那样,functools.wraps这是您要走的路。这有时会带来缺点,在使用typeshed的 IDE 类型检查器/linters 中,即使是简单函数的 IDE 中的类型提示也会发生变化,更改为不太好读的_Wrapped类型。您需要再次转换或覆盖该类型提示,例如使用与之一起使用的装饰器的签名。

@wraps(int)
def foo(x: str):
    return int(x)

reveal_type(foo)
# foo: _Wrapped[(x: ConvertibleToInt = ..., /), int, (x: str), int]

函数签名越长,就越丑陋。
是用来设置本例中以及其他属性的指示器_Wrapped。更准确地说,但取决于python版本:以及来自的所有条目。functools.update_wrapper`foo.__wrapped__ = intintfoo'__module__', '__name__', '__qualname__', '__doc__', '__annotations__'__dict__`

有时知道这一点很好,但有时却不方便,而且您想保留原始签名。


实际上有两种不同的情况 1)保持签名相同 2)注入额外参数。

对于 1),您可以使用绑定的 Callable TypeVar 或新的 3.12+ 语法:

Python <3.12
from typing import TypeVar, Callable
from functools import wraps

CT = TypeVar("CT", bound=Callable[..., Any])

def args_as_ints(f : CT) -> CT:
    """
    This tells the type-checker that the returned
    value is equivalent to the input value.
    """
    @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
...
Python 3.12+
from typing import Any, Callable
from functools import wraps

# Any is narrowed by the int from funny_function.
def args_as_ints[T: Callable[..., Any]](func: T) -> T:
    @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

...

reveal_type(funny_function)
# funny_function" is "(x: Any, y: Any, z: Any = 3) -> int"
2)扩展签名

保持类型提示相同的另一种方法是使用稍微不同的装饰器,并使用ParamSpec和functools.update_wrapper

from typing import TypeVar, Callable, ParamSpec, Concatenate
from functools import update_wrapper

T = TypeVar("T")
P = ParamSpec("P")


def args_as_ints(f : Callable[P, T]): # -> Callable[Concatenate[int, P], T]:
    # In this case you can also replace it with "int" to be explicit and not use
    # the signature of f, i.e. args: int
    def g(special_value=1, *args: P.args, **kwargs: P.kwargs) -> T:
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    update_wrapper(g, f)
    return g

ConvertibleToInt : TypeAlias = Any # you can do this properly

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

reveal_type(funny_function)
# "funny_function" is "(special_value: int = ... x: Any, y: Any, z: Any = ...) -> int"

在运行时这相当于@wraps

但是有一个优点和一个缺点:

优点:这是正确的显式类型,它有助于理解代码,并且可以避免错误。

缺点:您可能会丢失默认参数,因为您会看到z不再有默认值。带有的显式注释-> Callable[Concatenate[int, P], T]:会删除第一个参数名为的信息special_value。隐式类型/签名跟踪可以保留此信息,并且不会使用_Wrapped。当您在装饰器中注释或返回类型时,您会失去隐式跟踪。f

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1590  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1361  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   18  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   18  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   19  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用