在Python中设置函数签名

50 投票
8 回答
33177 浏览
提问于 2025-04-15 14:14

假设我有一个通用的函数 f。我想要程序化地创建一个函数 f2,它的行为和 f 一样,但函数的参数形式可以自定义。

更详细的说明

给定一个列表 l 和一个字典 d,我希望能够:

  • f2 的非关键字参数设置为 l 中的字符串
  • f2 的关键字参数设置为 d 中的键,并把默认值设置为 d 中的值

也就是说,假设我们有

l = ["x", "y"]
d = {"opt": None}

def f(*args, **kwargs):
    # My code

那么我想要一个这样的函数签名:

def f2(x, y, opt=None):
    # My code

一个具体的使用案例

这只是我具体使用案例的简化版本。我只是用这个作为例子。

我实际的使用案例(简化后)如下。我们有一个通用的初始化函数:

def generic_init(self, *args, **kwargs):
    """Function to initiate a generic object"""
    for name, arg in zip(self.__init_args__, args):
        setattr(self, name, arg)
    for name, default in self.__init_kw_args__.items():
        if name in kwargs:
            setattr(self, name, kwargs[name])
        else:
            setattr(self, name, default)

我们想在多个类中使用这个函数。特别是,我们想创建一个 __init__ 函数,它的行为像 generic_init,但函数签名是由某些类变量在创建时定义的:

class my_class:
    __init_args__ = ["x", "y"]
    __kw_init_args__ = {"my_opt": None}

__init__ = create_initiation_function(my_class, generic_init)
setattr(myclass, "__init__", __init__)

我们希望 create_initiation_function 创建一个新的函数,其签名是使用 __init_args____kw_init_args__ 定义的。是否可以编写 create_initiation_function

请注意:

  • 如果我只是想改善帮助文档,我可以设置 __doc__
  • 我们希望在创建时就设置函数签名。之后就不需要再改变了。
  • 我们可以创建一个新的函数,具有所需的签名,并且这个函数只是调用 generic_init,而不是创建一个像 generic_init 但签名不同的函数。
  • 我们希望定义 create_initiation_function。我们不想手动指定新的函数!

相关内容

8 个回答

7

我写了一个叫做 package forge 的工具,它正好可以解决这个问题,适用于 Python 3.5 及以上版本:

你现在的代码看起来是这样的:

l=["x", "y"]
d={"opt":None}

def f(*args, **kwargs):
    #My code

而你想要的代码应该是这样的:

def f2(x, y, opt=None):
    #My code

下面是如何使用 forge 来解决这个问题:

f2 = forge.sign(
    forge.arg('x'),
    forge.arg('y'),
    forge.arg('opt', default=None),
)(f)

因为 forge.sign 是一个封装工具,你也可以直接使用它:

@forge.sign(
    forge.arg('x'),
    forge.arg('y'),
    forge.arg('opt', default=None),
)
def func(*args, **kwargs):
    # signature becomes: func(x, y, opt=None)
    return (args, kwargs)

assert func(1, 2) == ((), {'x': 1, 'y': 2, 'opt': None})
63

根据 PEP-0362 的内容,其实在 Python 3.3 及以上版本中,有一种方法可以设置函数的签名,使用的是 fn.__signature__ 这个属性:

from inspect import signature
from functools import wraps

def shared_vars(*shared_args):
    """Decorator factory that defines shared variables that are
       passed to every invocation of the function"""

    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            full_args = shared_args + args
            return f(*full_args, **kwargs)

        # Override signature
        sig = signature(f)
        sig = sig.replace(parameters=tuple(sig.parameters.values())[1:])
        wrapper.__signature__ = sig

        return wrapper
    return decorator

然后:

>>> @shared_vars({"myvar": "myval"})
>>> def example(_state, a, b, c):
>>>     return _state, a, b, c
>>> example(1,2,3)
({'myvar': 'myval'}, 1, 2, 3)
>>> str(signature(example))
'(a, b, c)'

注意:这个 PEP 的描述并不完全正确;Signature.replace 方法把参数从位置参数变成了仅限关键字参数。

15

对于你的情况,在类或函数里加一个文档字符串应该就可以了——这样在使用help()时会显示出来,而且你也可以通过代码来设置(比如func.__doc__ = "内容")。

我没看到有什么方法可以设置实际的函数签名。我本以为functools模块能做到这一点,但在py2.5和py2.6中,它并没有提供这个功能。

如果你收到不好的输入,你也可以抛出一个类型错误(TypeError)异常。

嗯,如果你不介意用一些比较恶心的方法,可以使用compile()/eval()来实现。如果你想要的签名是arglist=["foo","bar","baz"],而你的实际函数是f(*args, **kwargs),你可以这样处理:

argstr = ", ".join(arglist)
fakefunc = "def func(%s):\n    return real_func(%s)\n" % (argstr, argstr)
fakefunc_code = compile(fakefunc, "fakesource", "exec")
fakeglobals = {}
eval(fakefunc_code, {"real_func": f}, fakeglobals)
f_with_good_sig = fakeglobals["func"]

help(f)               # f(*args, **kwargs)
help(f_with_good_sig) # func(foo, bar, baz)

更改文档字符串和函数名应该能给你一个完整的解决方案。不过,呃,这样做真的不太好...

撰写回答