【问题标题】:How to determine the order of passed keyword arguments?如何确定传递的关键字参数的顺序?
【发布时间】:2020-11-03 10:46:03
【问题描述】:

问题:

我想写一个通用函数:

def foo(positional, a=None, b=None, c=None, *, keyword_only=True):
    # ... ?? ... magic_code
    return a_b_c_in_tuple_in_order

返回一个保留关键字参数顺序的元组a,b,c

xx = 'some object'
>>> foo(xx, 1, 2, 3)
(1, 2, 3)
>>> foo(xx, 1, 2)
(1, 2)
>>> foo(xx, a=1, b=2, c=3, keyword_only=False)
(1, 2, 3)
>>> foo(xx, b=2, a=1, c=3)   # <---- key behaviour
(2, 1, 3)
>>> foo(xx, b=2, c=3)
(2, 3)
>>> foo(xx, c=3, a=1)
(3, 1)
>>> foo(xx, a='may be anything', c=range(5), b=[1, 2])
('may be anything', range(0, 5), [1, 2])
>>> foo(xx, b=1)
(1,)    # may be 1 or (1,)

我怎样才能做到这一点?这样的代码是非pythonic吗,如果是,我应该改用什么?

主要目标是易于使用和可读性。

为什么?

我的目标是使用这样的函数进行单位系统之间的转换(例如 SI 英制,但实际用例更高级),用户可以直观地编写例如

l, (t1, t2) = convert(params, lengths=L, times=(T1, T2), normalized=True)
# or
(t1, t2), l = convert(params, times=(T1, T2), lengths=L, normalized=True)

无论函数是如何定义的,以及数量是浮点数、数组等。

角落案例:

不需要对此类误用做出良好反应,但万无一失是一个好处:

>>> foo(b=2, a=1, c=3, positional=xx)
(2, 1, 3)
>>> foo(b=2, positional=xx, a=1, keyword_only=False, c=3)
(2, 1, 3)

【问题讨论】:

  • 用户如何使用这些元组?返回这些值的字典更加pythonic,因为关键字/命名参数没有排序。它们可以按任何顺序传递。更 Pythonic 的返回是:{"a":1,"b":2,"c":3},然后用户选择如何处理存储在每个键中的值。为什么订单对这里的用户很重要?
  • @spacether 用法将在 Why?* 部分中描述。我希望返回值是不可打包的,因此整个转换都在一个易于阅读的行内完成。
  • for conversions 有点模糊。拥有元组后如何处理它们?你不需要知道值对应的参数名称吗?
  • @spacether 哦,对不起,你是对的。我的意思是不同公制单位之间的转换(例如 SI - 英制,但实际用例更复杂)。
  • 实际上,如果您正在寻找单位转换,astropy 有一个非常强大和健壮的实现,它是described here,源代码是here。在我看来,这比依赖关键字参数的顺序要好得多,因此它可能对您有用。

标签: python python-3.x arguments parameter-passing


【解决方案1】:

@rdas 几乎拥有它 使用他们的答案作为装饰器可以保留原始函数签名并为您提供所需的数据:

kwargs_to_extract = {'a', 'b', 'c'}

def kwarg_tuple_returner(fn):
    def tuple_extractor(positional, *args, **kwargs):
        _unused_return = fn(positional, *args, **kwargs)
        if args and kwargs:
            return args + tuple(v for k, v in kwargs.items() if k in kwargs_to_extract)
        if args:
            return args
        if kwargs:
            return tuple(v for k, v in kwargs.items() if k in kwargs_to_extract)

    return tuple_extractor

@kwarg_tuple_returner
def foo(positional, a=None, b=None, c=None, *, keyword_only=True):
    # ... ?? ... magic_code
    # nothing below matters because we return our argument value from our decorator
    a = "mangled"
    b = 5
    c = 3.14
    return None

xx = 'obj'

print(foo(xx, 1, 2, 3))
print(foo(xx, 1, 2))
print(foo(xx, a=1, b=2, c=3, keyword_only=False))
print(foo(xx, b=2, a=1, c=3))
print(foo(xx, b=2, c=3))
print(foo(xx, c=3, a=1))
print(foo(xx, a='may be anything', c=range(5), b=[1, 2]))
print(foo(xx, b=1))
print(foo(b=2, a=1, c=3, positional=xx))
print(foo(b=2, positional=xx, a=1, keyword_only=False, c=3))
print(foo(xx, 1, c=2))

结果:

(1, 2, 3)
(1, 2)
(1, 2, 3)
(2, 1, 3)
(2, 3)
(3, 1)
('may be anything', range(0, 5), [1, 2])
(1,)
(2, 1, 3)
(2, 1, 3)
(1, 2)

【讨论】:

  • +1 因为它是保存函数签名的好方法。问题是降低了代码的可读性(并且代码有点乱),因为整个函数现在都在装饰器中,并且函数本身不能对这些值做任何事情。每个这样的函数都需要它自己的特定装饰器。
  • 您是否需要对许多功能进行此更改?函数 foo 可以对这些值做一些事情,我们将它们传递过去。在您的问题陈述中,您向我们展示了在 foo 中返回元组,因此您已经要求我们将 foo 的输出设为元组。
  • 我的意思是这种方法将整个函数定义移动到一个装饰器中,而实际定义本身只是一个占位符。
  • 但是代码仍然是一个很好的答案。 FWIW 在我的(科学)代码中,我定义了一堆这些来在一些(任意规范化的)单位系统之间进行转换。我没有在问题中包含这个,希望得到一个一般性的答案。也许我应该包括代码(当然)会根据传入的参数更改值。
  • 所以我们没有将整个函数移到装饰器中,只是将元组提取和返回。我们不需要运行 foo 所以我们在 foo 中执行 # ... ?? ... magic_code 吗?
【解决方案2】:

这似乎通过了你所有的例子,虽然我不确定positionalkeyword_only 的行为应该是什么:

def foo(positional, *args, **kwargs):
    if args and kwargs:
        return args + tuple(v for k, v in kwargs.items() if k in {'a', 'b', 'c'})
    if args:
        return args
    if kwargs:
        return tuple(v for k, v in kwargs.items() if k in {'a', 'b', 'c'})

xx = 'obj'

print(foo(xx, 1, 2, 3))
print(foo(xx, 1, 2))
print(foo(xx, a=1, b=2, c=3, keyword_only=False))
print(foo(xx, b=2, a=1, c=3))
print(foo(xx, b=2, c=3))
print(foo(xx, c=3, a=1))
print(foo(xx, a='may be anything', c=range(5), b=[1, 2]))
print(foo(xx, b=1))
print(foo(b=2, a=1, c=3, positional=xx))
print(foo(b=2, positional=xx, a=1, keyword_only=False, c=3))
print(foo(xx, 1, c=2))

结果:

(1, 2, 3)
(1, 2)
(1, 2, 3)
(2, 1, 3)
(2, 3)
(3, 1)
('may be anything', range(0, 5), [1, 2])
(1,)
(2, 1, 3)
(2, 1, 3)
(1, 2)

它依赖于python3中的字典是有序的。

【讨论】:

  • 这会破坏foo 的签名并且不适用于print(foo(xx, 1, c=2))
  • 对于它的价值,保留关键字参数的顺序与保持顺序的字典分开保证,尽管这得到了保持顺序的字典的帮助。参见 PEP 468。
  • 如果我可以明确定义参数变量,我会更高兴(可读性)。但是根据 ruhola 的 cmets 这似乎是不可能的,所以这是最好的方法。不过,如果有人弄清楚,我会稍等片刻。
  • @ruohola 是的。同时,这更适合误用类别 TBH。被破坏的签名更痛苦,但我可以接受这种妥协。
猜你喜欢
  • 1970-01-01
  • 2019-08-23
  • 2015-08-10
  • 1970-01-01
  • 2017-08-01
  • 2021-12-06
  • 1970-01-01
  • 2014-12-18
  • 2021-06-17
相关资源
最近更新 更多