【问题标题】:Set function signature in Python在 Python 中设置函数签名
【发布时间】:2010-11-27 09:56:52
【问题描述】:

假设我有一个通用函数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。我们不想手动指定新函数!

相关

【问题讨论】:

  • 您是在问如何以编程方式创建这样的功能吗?无论如何,我认为你需要给我们一些背景信息。你想如何使用它?你想如何指定函数的主体?你能告诉我们你想如何使用它吗?
  • 我重新编辑后是否更清楚了?
  • @Casebash:我认为这是一个非常简洁的功能。您有一个泛型函数,并且想要创建几个具有特定签名的非泛型函数。最简洁的方法就是创建它们。
  • @Casebash 你为此受到了很多嘲笑,但我发现这是一个合理的想法。不可行,但这个想法很酷。
  • 所有批评这个问题的人都需要立即去研究namedtuple。它完全完成了 OP 请求有关如何完成的信息的任务(以编程方式创建函数签名)。

标签: python function


【解决方案1】:

PEP-0362 来看,实际上似乎确实有一种方法可以使用fn.__signature__ 属性在py3.3+ 中设置签名:

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 将参数从位置 arg 移动到仅 kw arg。

【讨论】:

  • 考虑使用我在回答中建议的python-forge 包:stackoverflow.com/a/50533832/465164——我设计它是为了解决这个问题。
  • 试图使签名更大,由于意外的参数,我总是收到 TypeError。我可能会打开一个新问题,但如果有人有任何提示,请告诉我。
  • 确实有用。
【解决方案2】:

我写了一个 package named 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})

【讨论】:

  • 惊喜,惊喜,该软件包不再维护,尽管几年前@Devin 的大量广告。我很同情那些将这种依赖项添加到他们的项目中的人。
【解决方案3】:

看看makefun,它就是为此而生的(公开具有或多或少参数和准确签名的函数变体),并且适用于 python 2 和 3。

你的例子应该是这样写的:

try:  # python 3.3+
    from inspect import signature, Signature, Parameter
except ImportError:
    from funcsigs import signature, Signature, Parameter

from makefun import create_function

def create_initiation_function(cls, gen_init):
    # (1) check which signature we want to create
    params = [Parameter('self', kind=Parameter.POSITIONAL_OR_KEYWORD)]
    for mandatory_arg_name in cls.__init_args__:
        params.append(Parameter(mandatory_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD))
    for default_arg_name, default_arg_val in cls.__opt_init_args__.items():
        params.append(Parameter(default_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD, default=default_arg_val))
    sig = Signature(params)

    # (2) create the init function dynamically
    return create_function(sig, generic_init)

# ----- let's use it

def generic_init(self, *args, **kwargs):
    """Function to initiate a generic object"""
    assert len(args) == 0
    for name, val in kwargs.items():
        setattr(self, name, val)

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

my_class.__init__ = create_initiation_function(my_class, generic_init)

并按预期工作:

# check 
o1 = my_class(1, 2)
assert vars(o1) == {'y': 2, 'x': 1, 'my_opt': None}

o2 = my_class(1, 2, 3)
assert vars(o2) == {'y': 2, 'x': 1, 'my_opt': 3}

o3 = my_class(my_opt='hello', y=3, x=2)
assert vars(o3) == {'y': 3, 'x': 2, 'my_opt': 'hello'}

【讨论】:

  • 只是想让你知道:非常感谢这个库!我非常努力地在我的装饰器中获得正确的签名。没有任何效果,包括尝试使用 chriswarrick.com/blog/2018/09/20/… 作为灵感。在那里我看到了你的评论,到了你的库(后来在这里)。再次感谢!使用你的库是如此简单。
  • 感谢@Make42 的客气话!非常感谢
【解决方案4】:

我们希望create_initiation_function 更改签名

请不要这样做。

我们想在多个类中使用这个函数

请使用普通继承。

在运行时“更改”签名没有任何价值。

您正在制造维护的噩梦。没有人会费心去弄清楚你在做什么。他们会简单地将其撕掉并用继承替换它。

改为这样做。它简单明了,并以一种明显、简单、Python 的方式使您的通用 init 在所有子类中可用。

class Super( object ):
    def __init__( self, *args, **kwargs ):
        # the generic __init__ that we want every subclass to use

class SomeSubClass( Super ):
    def __init__( self, this, that, **kwdefaults ):
        super( SomeSubClass, self ).__init__( this, that, **kwdefaults )

class AnotherSubClass( Super ):
    def __init__( self, x, y, **kwdefaults ):
        super( AnotherSubClass, self ).__init__( x, y, **kwdefaults )

【讨论】:

    【解决方案5】:

    也许我没有很好地理解这个问题,但如果它是关于在更改函数签名的同时保持相同的行为,那么你可以这样做:

    # define a function
    def my_func(name, age) :
        print "I am %s and I am %s" % (name, age)
    
    # label the function with a backup name
    save_func = my_func
    
    # rewrite the function with a different signature
    def my_func(age, name) :
        # use the backup name to use the old function and keep the old behavior
        save_func(name, age)
    
    # you can use the new signature
    my_func(35, "Bob")
    

    这个输出:

    I am Bob and I am 35
    

    【讨论】:

    • 需要以编程方式。很难解释,但我想最后一次编辑不会杀了我
    【解决方案6】:

    你不能使用实时代码来做到这一点。

    也就是说,您似乎想要采用如下所示的实际实时函数:

    def f(*args, **kwargs):
        print args[0]
    

    把它改成这样:

    def f(a):
        print a
    

    无法做到这一点的原因——至少在不修改实际 Python 字节码的情况下——是因为它们的编译方式不同。

    前者产生一个接收两个参数的函数:一个列表和一个字典,而您正在编写的代码在该列表和字典上运行。第二个结果是一个函数,它接收一个参数,并直接作为局部变量访问。如果你改变了函数“签名”,可以这么说,它会产生这样的函数:

    def f(a):
        print a[0]
    

    这显然行不通。

    如果你想要更多细节(虽然它对你没有帮助),一个接受 *args 或 *kwargs 的函数在f.func_code.co_flags 中设置一个或两个位;你可以自己检查一下。采用常规参数的函数将f.func_code.co_argcount 设置为1; *args 版本为 0。这是 Python 用来确定如何在调用函数时设置函数的堆栈帧、检查参数等的方法。

    如果您想尝试直接修改函数——如果只是为了让自己相信它不起作用——请参阅this answer,了解如何从现有对象创建代码对象和实时函数以修改位其中。 (这些东西记录在某处,但我找不到它;它不在类型模块文档中...)

    也就是说,您可以动态地更改函数的文档字符串。只需分配给func.__doc__。确保仅在加载时执行此操作(从全局上下文或 - 最有可能 - 装饰器);如果您稍后再执行此操作,加载模块以检查文档字符串的工具将永远看不到它。

    【讨论】:

    • 我不需要动态更改函数的签名,只是为了能够创建一个与另一个函数执行相同但具有不同签名的函数
    • 不完全。正如 Towns 所指出的,我们可以使用 eval 创建一个可以调用泛型函数的新函数
    【解决方案7】:

    编辑 1:回答新问题:

    您问如何使用此签名创建函数:

    def fun(a, b, opt=None):
        pass
    

    因此,在 Python 中执行此操作的正确方法是:

    def fun(a, b, opt=None):
        pass
    

    编辑2:回答解释:

    “假设我有一个通用函数 f。我想以编程方式创建一个与 f 行为相同但具有自定义签名的函数 f2。”

    def f(*args, **kw):
        pass
    

    好的,那么 f2 看起来像这样:

    def f2(a, b, opt=None):
        f(a, b, opt=opt)
    

    同样,您的问题的答案是如此琐碎,以至于您显然想知道与您所要求的不同的东西。你真的需要停止问抽象的问题,并解释你的具体问题。

    【讨论】:

    • 不回答问题。很抱歉不清楚,我现在已经澄清了。
    • 它确实回答了这个问题。我还更新了答案以反映您的更改。它再次回答了这个问题。它可能不会告诉你你想知道什么,但你必须先告诉我们你想知道什么。
    • 我很确定我编辑后意思很清楚,但我又编辑了。
    • 不,还不清楚。 “创建函数”是什么意思?该函数应该做什么?
    • 好的,对不起所有让我感到困惑的人。我真的希望这次我已经设法让自己清楚了。
    【解决方案8】:

    对于您的用例,在类/函数中有一个文档字符串应该可以工作——它会显示在 help() 中,可以通过编程方式设置 (func.__doc__ = "stuff")。

    我看不到任何设置实际签名的方法。如果可行,我会认为functools module 会这样做,但至少在 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)
    

    更改 docstring 和 func_name 应该会给您一个完整的解决方案。但是,呃,呃……

    【讨论】:

    猜你喜欢
    • 2020-01-09
    • 1970-01-01
    • 2011-05-24
    • 1970-01-01
    • 2019-07-21
    • 1970-01-01
    • 1970-01-01
    • 2011-03-03
    • 1970-01-01
    相关资源
    最近更新 更多