在Python中处理灵活的函数参数

时间:2014-12-03 12:32:48

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

TL; TR 寻找习惯用法和模式,根据简单的规范,将位置和关键字参数解包为位置参数的有序序列,例如:名单。这个想法看起来类似于类似scanf的解析。

我正在包装一个名为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)

这是我第一次尝试为调用foo的{​​{1}}包装器实现此类参数处理:

someapi.foo

据我所知,它完成了预期的工作,但它有两个问题:

  1. 我可以用更多 Python惯用方式做得更好吗?
  2. 我有十几个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) 函数要包装,那么如何避免在每个包装器中复制和调整 BEGIN / END 标记之间的整个块?
  3. 我还不知道问题1的答案。

    但是,我试图解决这个问题2.

    因此,我根据someapi的简单规范为参数定义了一个通用处理程序。 names指定了几个东西,具体取决于实际的包装器调用:

    • names解压缩多少个参数? (参见下面的*args测试)
    • len(names)中预计会包含哪些关键字参数? (参见generator expression返回下面的元组)

    这是新版本:

    **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可以使用7个,8个以上的位置参数或一长串数字来调用,但只考虑前6个。例如,它允许迭代处理长数字列表(例如,想到几何坐标):

    bar
    • 关键字参数的灵活协议:可以指定比实际使用的更多关键字,或者字典可以包含的项目多于使用的项目。

    回到上面的问题1,我能做得更好,让它变得更像Pythonic吗?

    另外,我想请求审核我的解决方案:你看到有什么错误吗?我忽略了什么吗?如何改进?

1 个答案:

答案 0 :(得分:4)

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)那么我们可以自动执行{{1模块my_wrapper_maker导出的所有内容。我们会这样做:

someapi

这可能是最常用的 pythonic 方式,它充分利用了Python的元编程资源,并允许程序员在任何地方使用这个API,而不依赖于特定的{{1 }}

注意:这是否是大多数惯用方式来实现这一点,这真的取决于意见。我个人认为这符合“禅宗之谜”中提出的哲学,所以对我来说这是非常惯用的。