如何动态修改函数的签名

问题描述 投票:0回答:2

我正在用Python编写一个框架。当用户声明一个函数时,他们会:

def foo(row, fetch=stuff, query=otherStuff)

def bar(row, query=stuff)

def bar2(row)

当后端看到 query= value 时,它会根据 value 执行具有查询参数的函数。这样,该函数就可以访问后端在其范围内完成的操作的结果。

目前,我每次都会通过检查查询、获取和其他项目是否为“无”来构建参数,并使用一组与用户要求完全匹配的参数来启动它。否则我会收到“意外的关键字参数”错误。这是后端的代码:

#fetch and query is something computed by the backend
if fetch= None and query==None:
    userfunction(row)
elif fetch==None:
    userunction (row, query=query)
elif query == None:
    userfunction (row, fetch=fetch)
else:
    userfunction (row,fetch=fetch,query=query)

这不好;对于后端提供的每个附加“服务”,我需要编写与之前的所有组合。

相反,我想主要采用该函数并在执行之前手动添加命名参数,删除执行这些检查的所有不必要的代码。然后用户就会使用他真正想要的东西。

我不希望用户必须通过添加不需要的内容来修改函数(我也不希望他们每次都指定

kwarg
)。

所以如果可行的话,我想要一个这样的例子,一个函数

addNamedVar(name, function)
,将变量
name
添加到函数
function

我想这样做,因为用户函数被调用很多次,这意味着它会触发我,例如,创建函数的命名 var 的字典(带有检查),然后使用

**dict
。我真的很想只修改一次函数以避免任何类型的开销。

python-3.x arguments abstract-syntax-tree python-internals
2个回答
0
投票

这在 AST 中确实可行,这就是我要做的,因为这个解决方案更适合我的用例。但是,您可以通过使用函数克隆方法(如我展示的代码片段)来更简单地完成我所要求的操作。请注意,此代码返回具有不同默认值的相同函数。您可以使用此代码作为示例来执行您想做的任何操作。 这适用于 python3

def copyTransform(f, name, **args):
    signature=inspect.signature(f)
    params= list(signature.parameters)
    numberOfParam= len(params)
    numberOfDefault= len(f.__defaults__)
    listTuple= list(f.__defaults__)

    for key,val in args.items():
        toChangeIndex = params.index(key, numberOfDefault)
        if toChangeIndex:
            listTuple[toChangeIndex- numberOfDefault]=val

    newTuple= tuple(listTuple)
    oldCode=f.__code__

    newCode= types.CodeType(
        oldCode.co_argcount,             #   integer
        oldCode.co_kwonlyargcount,       #   integer
        oldCode.co_nlocals,              #   integer
        oldCode.co_stacksize,            #   integer
        oldCode.co_flags,                #   integer
        oldCode.co_code,                 #   bytes
        oldCode.co_consts,               #   tuple
        oldCode.co_names,                #   tuple
        oldCode.co_varnames,             #   tuple
        oldCode.co_filename,             #   string
        name,                            #   string
        oldCode.co_firstlineno,          #   integer
        oldCode.co_lnotab,               #   bytes
        oldCode.co_freevars,             #   tuple
        oldCode.co_cellvars              #   tuple
        )

    newFunction=types.FunctionType(newCode, f.__globals__, name, newTuple, f.__closure__)
    newFunction.__qualname__=name #also needed for serialization

如果你想腌制你的克隆函数,你需要对名称做一些奇怪的事情。


0
投票

您可以通过替换

__code__
属性值来更改函数的签名,例如使用方便的
replace
方法:

def f(a, b, c):
    return (a, b, c)

f.__code__ = f.__code__.replace(co_varnames=f.__code__.co_varnames + ('d',), co_nlocals=4, co_argcount=4) # Add the `d` parameter and adjust related property values correspondingly

但是,对于大多数愿意冒险那么远的人来说,结果可能无法完全预测,特别是如果您不熟悉 Python 字节码。

编译后的函数体通过索引来寻址参数,而不是通过名称

如果您通过在函数参数集中插入一个参数来修改函数,就可以证明这一点,特别是

在前面

def f(a, b, c): return (a, b, c) f.__code__ = f.__code__.replace(co_varnames=('d',) + f.__code__.co_varnames, co_nlocals=4, co_argcount=4) # Observe that `d` is added _before_ the rest of the original set of parameters, in contrast to the expression in the first snippet
现在,根据 

f

 的原始定义,调用 
f('A', 'B', 'C')
 将返回元组 
('A', 'B', 'C')
,对修改后的 
f
f('D', 'A', 'B', 'C')
 的调用将返回元组... 
('D', 'A', 'B')
f
 通过有序参数列表中的相同索引访问参数,该索引最初是在编译 
f
 时计算的!

这就是为什么在这种情况下使用

__code__

 修改签名既不是最有用,也不是“安全”。

如果您使用

dis

 模块及其导出的同名 
dis
 函数,您可以看到反汇编结果显示错误的索引到已修改 
f
 名称的“移位”映射(根据最后一个片段):

>>> dis.dis(f) 1 0 RESUME 0 2 2 LOAD_FAST 0 (d) 4 LOAD_FAST 1 (a) 6 LOAD_FAST 2 (b) 8 BUILD_TUPLE 3 10 RETURN_VALUE
(三个连续的

LOAD_FAST

指令分别获取
d
a
b
,尽管编译源表示“返回
a
b
c
”)

所以总而言之,当然——你可以修改函数的签名——但要知道会发生什么。

© www.soinside.com 2019 - 2024. All rights reserved.