【问题标题】:Handling flexible function arguments in Python在 Python 中处理灵活的函数参数
【发布时间】:2015-02-01 00:36:15
【问题描述】:

TL;TR 根据简单的规范,例如,寻找将位置参数和关键字参数解压缩为位置参数的有序序列的习语和模式名单。这个想法似乎类似于 scanf-like 解析。

我正在包装名为someapi 的 Python 模块的函数。 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 中需要哪些关键字参数? (参见下面的 generator expression 返回元组)

这是新版本:

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 吗?

另外,我想要求对我的解决方案进行审核:您发现任何错误吗?我忽略了什么吗?如何改进?

【问题讨论】:

  • 这是一个奇怪的期望行为。作为调用者,如果我用foo(1, 2, 3, 4, 5) 调用函数,我会惊讶地发现我的一个论点被忽略了。例如,对于调用foo(1, 2, 3) 并使用第四个默认参数,我不会三思而后行,但删除参数很奇怪。为什么您认为此 API 的用户调用参数数量不正确的函数?
  • @Cyber​​ 我理解你的理由。两件事:1)这种扩展协议是次要优势 2)但是,如果可用,它有用例。我添加了带有 meaw 函数的示例,说明了一个这样的用例。所以,这不是无视论点,而是我认为它更像是隐式切片。当然,它必须记录在案,以便我的包装器的用户知道这样的功能。
  • 如果用户同时添加位置参数和关键字参数会发生什么?我猜是“无效参数”?
  • 顺便说一句,如果用户没有指定所有指定的关键字参数,比如foo(x=1, yismissing=2, z=3, r=4),他会得到一个 KeyError。不确定这里的预期行为是什么。
  • 除了我认为最大的调用者灵活性是 YAGNI 甚至可能是错误之外,我没有什么可以添加到上面的 cmets 中。要求调用者执行foo(*[1,2,3,4,5])foo(**{'x':1,'y':2}) 并不太繁重,并且消除了代码和文档需求。

标签: python design-patterns python-3.x arguments idioms


【解决方案1】:

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 处理错误。

现在让它更 Pythonic。使用同一模板包装多个函数的理想方法是使用 装饰器语法(假设您熟悉此主题,如果您想了解更多信息,请参阅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)

这可能被认为是最pythonic的实现方式,它充分利用了Python的元编程资源,并允许程序员在任何他们想要的地方使用这个API,而不依赖于特定的someapi .

注意:这是否是最惯用的方法,这完全取决于意见。我个人认为这很好地遵循了“Python之禅”中提出的哲学,所以对我来说它非常地道。

【讨论】:

  • 我很高兴接受您的回答。您提出了对我来说相当新的解决方案,尤其是在这种情况下使用检查。当然,我认为它是我提出的优雅和 Pythonic 的替代方案。如果它是惯用的,我没有足够的经验来判断,但我很满意。谢谢!
猜你喜欢
  • 2012-01-21
  • 1970-01-01
  • 1970-01-01
  • 2022-01-15
  • 2019-07-14
  • 2019-09-19
  • 2012-09-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多