【问题标题】:Pass extra optional arguments to callbacks without breaking existing callbacks在不破坏现有回调的情况下将额外的可选参数传递给回调
【发布时间】:2019-01-08 22:03:34
【问题描述】:

我有一个接受回调的 API 方法。回调需要一个参数。

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

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

【问题讨论】:

    标签: python python-3.x python-3.3


    【解决方案1】:

    我认为您可以使用 __code__ 来查看回调需要多少参数。

    if callback.__code__.co_argcount == 2:
        callback(arg1, arg2)
    else:
        callback(arg1)
    

    此代码未经测试,但应该可以工作。

    【讨论】:

    • 很好的解决方案。我忘记了函数对象的__code__ 属性。
    • 谢谢@blhsing。您的 try..except 解决方案也适用,但如果经常调用异常,则性能会受到影响。
    • 同意。当回调只接受一个参数时,try-except 解决方案的效率要低得多,因为它会在创建异常和回溯对象时增加开销。
    • 我不知道__code__.co_argcount。看起来您可以进一步简化/概括它:callback(*args[:callback.__code__.co_argcount]).
    • @thom-smith 如果您通过回调传递参数,那就对了。使用“真实”回调,您不能直接传递任何内容。可以传递参数的是调用者。
    【解决方案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)
    

    【讨论】:

    • 这样做的问题是,如果callback(first, second) 出于任何其他原因引发TypeError,则该错误将被丢弃,callback 将被第二次调用。
    • 是的,尽管编写良好的回调函数一开始就不应该引发 TypeError。但是,如果这确实是一个问题,您可以通过检查异常消息来确定 TypeError 是否是由使用错误数量的参数调用引起的,如我更新的答案中所示)。它确实使代码看起来不那么优雅,但它可以工作,并且没有理由相信 Python 编译器会针对现有的内置异常更改其错误消息。
    • 我实际上有一个未解决的问题,即在 3.6+ 中测试失败,因为内置异常消息已更改。此外,callback 可能会产生一个无法区分的 TypeError 与相同的消息。 (诚​​然,这些问题在实践中不太可能出现。)
    • 我突然想到,这个解决方案和我的回答中更复杂的解决方案之间的中间立场是使用inspect.Signature.bind。您可以在尝试运行回调之前捕获 TypeError
    • 是的。我已经更新了我的答案以使用回溯对象。将 func_name 替换为使用此代码的函数的名称。
    【解决方案3】:

    使用函数包装器:

    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])
    

    【讨论】:

      猜你喜欢
      • 2012-01-04
      • 1970-01-01
      • 2017-04-09
      • 1970-01-01
      • 2014-03-25
      相关资源
      最近更新 更多