Python中处理灵活的函数参数

3

TL;TR 寻找将位置和关键字参数解包为基于简单规范(例如名称列表)的有序位置参数序列的习语和模式。这个想法似乎类似于scanf样式的解析。

我正在封装Python模块someapi的函数。 someapi的函数只接受位置参数,在大多数情况下是痛苦的数字。 我希望能够让调用者以灵活的方式传递参数给我的包装器。 以下是我想允许的包装器调用示例:

# foo calls someapi.foo()
foo(1, 2, 3, 4)
foo(1, 2, 3, 4, 5) # but forward only 1st 4 to someapi.foo
foo([1, 2, 3, 4])
foo([1, 2, 3, 4, 5, 6]) # but forward only 1st 4 to someapi.foo
foo({'x':1, 'y':2, 'z':3, 'r':4})
foo(x=1, y=2, z=3, r=4)
foo(a=0, b=0, x=1, y=2, z=3, r=4) # but forward only x,y,z,r someapi.foo

我不认为有必要支持混合定位和关键词参数的复杂情况:
foo(3, 4, x=1, y=2)

这是我第一次尝试为调用someapi.foofoo包装器实现此类参数处理:

def foo(*args, **kwargs):
    # BEGIN arguments un/re-packing
    a = None
    kwa = None
    if len(args) > 1:
        # foo(1, 2, 3, 4)
        a = args
    elif len(args) == 1:
        if isinstance(args[0], (list, tuple)) and len(args[0]) > 1:
            # foo([1, 2, 3, 4])
            a = args[0]
        if isinstance(args[0], dict):
            # foo({'x':1, 'y':2, 'z':3, 'r':4})
            kwa = args[0]
    else:
        # foo(x=1, y=2, z=3, r=4)
        kwa = kwargs

    if a:
        (x, y, z, r) = a
    elif kwa:
        (x, y, z, r) = (kwa['x'], kwa['y'], kwa['z'], kwa['r'])
    else:
        raise ValueError("invalid arguments")
    # END arguments un/re-packing

    # make call forwarding unpacked arguments 
    someapi.foo(x, y, z, r)

我认为这个程序已经按照预期完成了任务,但是有两个问题:
  1. 我能否用更符合Python惯用法的方式来实现它?
  2. 我需要包装数十个someapi函数,如何避免在每个包装器中复制和调整整个BEGIN/END块?

对于问题1,我不知道答案。

然而,我尝试解决问题2。

因此,我定义了一个基于names的简单规范的通用参数处理程序。 names根据实际的包装器调用指定了一些内容:

  • *args中解包多少个参数?(请参见下面的len(names)测试)
  • **kwargs中期望哪些关键字参数?(请参见下面返回元组的生成器表达式

这是新版本:

def unpack_args(names, *args, **kwargs):
    a = None
    kwa = None
    if len(args) >= len(names):
        # foo(1, 2, 3, 4...)
        a = args
    elif len(args) == 1:
        if isinstance(args[0], (list, tuple)) and len(args[0]) >= len(names):
            # foo([1, 2, 3, 4...])
            a = args[0]
        if isinstance(args[0], dict):
            # foo({'x':1, 'y':2, 'z':3, 'r':4...})
            kwa = args[0]
    else:
        # foo(x=1, y=2, z=3, r=4)
        kwa = kwargs
    if a:
        return a
    elif kwa:
        if all(name in kwa.keys() for name in names):
            return (kwa[n] for n in names)
        else:
            raise ValueError("missing keys:", \
                [name for name in names if name not in kwa.keys()])
    else:
        raise ValueError("invalid arguments")

这使我能够按照以下方式实现包装函数:
def bar(*args, **kwargs):
    # arguments un/re-packing according to given of names
    zargs = unpack_args(('a', 'b', 'c', 'd', 'e', 'f'), *args, **kwargs)
    # make call forwarding unpacked arguments 
    someapi.bar(*zargs)

我认为我已经实现了比上面的foo版本更多的优点:

  • 使调用者具有所需的灵活性。

  • 紧凑的形式,减少复制和粘贴。

  • 位置参数的灵活协议:bar可以使用7、8个或更多位置参数或长列表进行调用,但只考虑前6个。例如,它将允许迭代处理长列表的数字(例如,考虑几何坐标):

    # meaw expects 2 numbers
    n = [1,2,3,4,5,6,7,8]
    for i in range(0, len(n), 2):
        meaw(n[i:i+2])
  • 灵活的关键字参数协议:可以指定比实际使用更多的关键词,或者字典中可以有比使用更多的项。

回到上面的问题1,我能做得更好并使其更符合Pythonic吗?

此外,我想请求对我的解决方案进行审查:是否存在任何错误?我是否忽略了什么?如何改进它?


6
这是一种奇怪的预期行为。作为调用者,如果我使用“foo(1,2,3,4,5)”调用函数,发现其中一个参数被忽略,我会感到惊讶。例如,我不会对调用“foo(1,2,3)”并具有第四个默认参数感到困惑,但删除参数则很奇怪。你为什么预期此API的用户将使用不正确数量的参数来调用函数呢? - Cory Kramer
@Cyber 我理解你的想法。有两点需要说明:1)这种扩展协议是次要的优势;2)但是,如果可用,它确实有用例。我已经添加了一个使用 meaw 函数的示例,以说明其中一种用例。因此,这并不是忽略参数,而是将其视为隐式切片。当然,必须记录文档,以便我的包装器的用户知道这种功能。 - mloskot
1
如果用户同时添加位置参数和关键字参数,应该发生什么?我猜是"无效参数"吧? - KurzedMetal
1
顺便提一下,如果用户没有指定所有期望的关键字参数,比如 foo(x=1, yismissing=2, z=3, r=4),他将会得到一个 KeyError。不确定这里的预期行为是什么。 - KurzedMetal
5
除了我的观点是最大限度地提供调用者灵活性可能是一个YAGNI (应该不需要) 甚至也可能是一个错误之外,我没有什么要补充上述评论。要求调用者执行 foo(* [1,2,3,4,5])foo(** {'x':1,'y':2}) 不会太繁琐,并可以减少代码和文档的需求。 - sgillies
显示剩余2条评论
1个回答

5

Python是一种非常强大的语言,可以让您以任何方式操纵代码,但理解您正在做什么很难。为此,您可以使用inspect模块。因此,在someapi中包装函数的示例如下。在此示例中,我仅考虑位置参数,您可以直觉地将其扩展。您可以像这样进行操作:

import inspect
import someapi

def foo(args*):
    argspec = inspect.getargspec(someapi.foo)

    if len(args) > len(argspec.args):
        args = args[:len(argspec.args)]

    return someapi.foo(*args)

这将检测传递给 foo 的参数数量是否过多,如果是,则会摆脱多余的参数。另一方面,如果参数过少,则什么也不做,让 foo 处理错误。
现在让它更符合 Python 的风格。使用相同模板包装许多函数的理想方式是使用 装饰器语法(假定您熟悉此主题,如果想了解更多信息,请参见http://www.python.org/doc中的文档)。虽然由于 装饰器语法 主要用于开发中的函数而不是包装另一个 API,但我们将创建一个装饰器,但只将其用作 API 的工厂(工厂模式)。为了创建这个工厂,我们将利用 functools 模块来帮助我们(使包装后的函数看起来符合规范)。因此,我们可以将示例转换为:
import inspect
import functools
import someapi

def my_wrapper_maker(func):
    @functools.wraps(func)
    def wrapper(args*):
        argspec = inspect.getargspec(func)

        if len(args) > len(argspec.args):
            args = args[:len(argspec.args)]

        return func(*args)
    return wrapper

foo = my_wrapper_maker(someapi.foo)

最后,如果someapi有一个相对较大的API,在版本之间可能会发生变化(或者我们只是想使我们的源文件更加模块化,以便它可以包装任何API),那么我们可以自动应用my_wrapper_maker到由模块someapi导出的所有内容。这样做:

__all__ = ['my_wrapper_maker']

# Add the entire API of someapi to our program.
for func in someapi.__all__:
    # Only add in bindings for functions.
    if callable(getattr(someapi, func)):
        globals()[func] = my_wrapper_maker(getattr(someapi, func))
        __all__.append(func)

这可能被认为是实现这一功能最符合Python风格的方式,它充分利用了Python元编程资源,并允许程序员在任何地方使用此API,而不依赖于特定的“someapi”。
注意:是否这是最符合惯例的方法实现取决于个人观点。我个人认为,这很好地遵循了《Python之禅》中设定的哲学,因此对我来说非常符合惯例。

我很高兴接受你的答案。你提出的解决方案对我来说是比较新的,尤其是在这种情况下使用inspect。无疑,我认为它是一个优雅且符合Python风格的替代方案,相比我提出的方案更好。至于是否符合惯用法,我经验不足无法判断,但我感到满意。谢谢! - mloskot

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