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
据我所知,它完成了预期的工作,但它有两个问题:
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 标记之间的整个块?我还不知道问题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吗?
另外,我想请求审核我的解决方案:你看到有什么错误吗?我忽略了什么吗?如何改进?
答案 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 }}
注意:这是否是大多数惯用方式来实现这一点,这真的取决于意见。我个人认为这符合“禅宗之谜”中提出的哲学,所以对我来说这是非常惯用的。