【发布时间】: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.foo 的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)
据我所知,它按预期完成了工作,但存在两个问题:
- 我可以用更 Python 惯用 的方式做得更好吗?
- 我有几十个
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