不破坏现有回调函数的情况下向回调函数传递额外的可选参数。

5

我有一个接受回调函数的API方法。回调函数期望一个参数。

我希望这个方法传递第二个参数给那些需要它的回调函数。但是,我必须保持与仅接受原始参数的回调函数的兼容性。(事实上,我预计大多数用户都不会关心额外的参数,因此强制他们明确忽略它将是很烦人的。)

我知道可以使用 inspect 来完成这个任务。但我想知道是否有一个“惯用”的或常用的解决方案,不太重量级。

3个回答

5
我认为您可以使用 __code__ 来查看回调函数需要多少个参数。
if callback.__code__.co_argcount == 2:
    callback(arg1, arg2)
else:
    callback(arg1)

这段代码没有经过测试,但应该可以正常工作。

很棒的解决方案。我忘记了函数对象的__code__属性。 - blhsing
谢谢@blhsing。你的try..except解决方案也可以工作,但如果异常频繁调用会有性能惩罚。 - Christoforus Surjoputro
同意。当回调函数只接受一个参数时,try-except解决方案的效率要低得多,因为它会增加异常和回溯对象的创建开销。 - blhsing
我之前不知道__code__.co_argcount。看起来你甚至可以更进一步地简化/概括它:callback(*args[:callback.__code__.co_argcount]) - Thom Smith
@thom-smith,如果你通过回调传递参数是正确的。使用“真正”的回调函数,你无法直接传递任何内容。能够传递参数的是调用者。 - Christoforus Surjoputro

2
一个更简单的解决方案是使用一个try块,首先尝试使用第二个参数调用回调函数,然后在except块中备用,只使用一个参数调用回调函数。
try:
    callback(first, second)
except TypeError as e:
    if e.__traceback__.tb_frame.f_code.co_name != 'func_name':
        raise
    callback(first)

1
问题在于,如果callback(first, second)由于任何其他原因引发了TypeError,那么该错误将被丢弃,并且callback将再次被调用。 - Thom Smith
True,虽然一个写得好的回调函数应该从一开始就不会引发TypeError。但如果这真的是一个问题,你可以通过检查异常消息来确定TypeError是否是由于使用错误数量的参数调用而引起的(如我的更新答案所示)。这确实使代码看起来不太优雅,但它会起作用,并且没有理由相信Python编译器会为现有的内置异常更改其错误消息。 - blhsing
我实际上有一个未解决的问题,即在3.6+中测试失败,因为内置异常消息已更改。此外,callback可能会产生一个具有相同消息的无法区分的TypeError。(这些问题在实践中被承认不太可能出现。) - Thom Smith
我突然想到这个解决方案和我的答案中更复杂的解决方案之间的折衷方案是使用 inspect.Signature.bind。在尝试运行回调函数之前,您可以捕获 TypeError - Thom Smith
真的。我已经更新了我的答案,使用traceback对象替换。将func_name替换为此代码所使用的函数的名称。 - blhsing

-1
使用函数包装器:
from inspect import signature, Parameter

def ignore_extra_arguments(function):
    positional_count = 0
    var_positional = False
    keyword_names = set()
    var_keyword = False

    for p in signature(function).parameters.values():
        if p.kind == Parameter.POSITIONAL_ONLY:
            positional_count += 1
        elif p.kind == Parameter.POSITIONAL_OR_KEYWORD:
            positional_count += 1
            keyword_names.add(p.name)
        elif p.kind == Parameter.VAR_POSITIONAL:
            var_positional = True
        elif p.kind == Parameter.KEYWORD_ONLY:
            keyword_names.add(p.name)
        elif p.kind == Parameter.VAR_KEYWORD:
            var_keyword = True

    if var_positional:
        new_args = lambda args: args
    else:
        new_args = lambda args: args[:positional_count]

    if var_keyword:
        new_kwargs = lambda kwargs: kwargs
    else:
        new_kwargs = lambda kwargs: {
            name: value for name, value in kwargs.items()
            if name in keyword_names
        }

    def wrapped(*args, **kwargs):
        return function(
            *new_args(args),
            **new_kwargs(kwargs)
        )

    return wrapped

它能够工作,但有点粗暴。

更简单的版本,假设 function 没有关键字参数或可变参数:

from inspect import signature

def ignore_simple(function):
    count = len(signature(function).parameters)
    return lambda *args: function(*args[:count])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接