【问题标题】:Packing values into a tuple using *, just like function argument packing使用 * 将值打包到元组中,就像函数参数打包一样
【发布时间】:2020-04-09 02:46:02
【问题描述】:

考虑一个定义为的函数:

def fun(a, *args):
    print(type(args), args)

调用时,它将额外的位置参数打包为一个元组

>>> fun(2, 3, 4)
<class 'tuple'> (3, 4)

我想在函数参数之外实现类似的东西。所以,我认为我可以通过扩展的可迭代解包来实现相同的目标,但它总是将内容打包为 list 而从不打包为 tuple

# RHS is a tuple
>>> (a, *args) = (2, 3, 4)
>>> type(args)
<class 'list'>
>>> args
[3, 4]  #but args is not a tuple!

使其与以下内容没有什么不同:

# RHS is a list
>>> (a, *args) = [2, 3, 4]
>>> type(args)
<class 'list'>
>>> args
[3, 4]

我可以理解,这也是PEP中提出的方式。

我在问是否有其他方法可以实现我想要的。

当然,我可以稍后将args 转换为元组:

>>> args = tuple(args)
>>> args
(3, 4)

如果在分配期间无法直接实现。

【问题讨论】:

标签: python python-3.x tuples iterable-unpacking argument-unpacking


【解决方案1】:

我知道的最接近的方法(我承认,并没有直接回答你的问题)只是对元组进行切片:

t = (2,3,4)
a, args = t[0], t[1:]

>>> type(args)
<class 'tuple'>

是的,它很冗长,但至少它避免了中间列表。

【讨论】:

  • 让我们看看是否有更清洁的解决方案。 PEP 特别提到这种方法不太干净且效率较低:许多算法需要将序列拆分为“第一,后”对。使用语法, first, rest = seq[0], seq[1:] 被更干净的并且可能更有效的替换: first, *rest = seq
  • @inquisitiveOne 无论如何。我并不是说这是最好的解决方案,但这是我目前唯一能想到的解决方案。我很好奇它是否实际上效率明显降低。我想不出一个为什么会更高效的原因,除非在每个中卸载到 C 的工作量有所不同。
  • 另一方面,如果用t[1:] 构造一个新元组是你代码中的瓶颈,那么你的代码几乎肯定足够高效。
  • @Carcigenicate:我相信成本的差异仅在于固定开销,因此不会影响 big-O 运行时; first, rest = seq[0], seq[1:] 涉及更多的字节码(11 条指令,first, *rest = seq 为 4 条指令),但无论哪种方式完成的工作都大致相同。碰巧的是,至少对于tuples,first, rest = seq[0], seq[1:] 在微基准测试中确实更快(在我的 CPython 3.8.0 Linux x64 上),因为它可以使用专用的tuple 切片代码,其中解包使用通用迭代器消耗代码构建listfirst, *rest 更通用。
【解决方案2】:

这里还有一些额外的注意事项需要牢记。 RHS 的类型不限于列表或元组。您可以解压缩任意可迭代对象:

a, *args = itertools.repeat('hi', 3)

没有充分的理由认为args 应该是任何一种类型。结果是一个列表是有道理的,因为它应该能够累积任意数量的元素,但在 C 代码中没有重大相关差异,至少在 CPython 中是这样。

函数参数几乎必须是一个元组,因为函数可以返回打包的对象和一个闭包。在这种情况下,您不希望能够修改 args

def func(arg, *args):
    assert args
    def blah():
        return args[0]
    return args, blah

这有点做作,但说明了为什么 args 可以被认为是公共的并且在函数中不应该是可变的。

话虽如此,这里有一个通用的方法来获得你想要的解包,不管 RHS 类型如何:

t = (2, 3, 4)
it = iter(t)
a, args = next(it), tuple(it)

【讨论】:

  • 这不是“函数参数几乎必须是元组”的充分理由;如果您需要这种行为,并不是不能将args = tuple(args) 添加为第一行。我怀疑它们是 tuples 只是因为它们通常很小,而且 CPython 参考解释器对小型 tuples 进行了一些优化,并且因为它使 CPython C 代码更容易;您不必担心tuple 在处理过程中可能会被调整大小或更改,即使您调用 Python 级别的代码(这可能会隐含地发生在长度检查等简单的事情中)。
  • 注意:我喜欢你指出a, *args = x 不能依赖于x 的类型。我确实认为他们选择list 很奇怪,因为它与函数参数案例一样可变;将真正的可变输入转换为list 可能会稍微快一些,但还不够重要。旁注:在 3.8 中,您可以将通用方法单行改为:a, args = next(it := iter(t)), tuple(it),甚至a, args = next(it := iter(t)), (*it,)。不是真的推荐它,但它是一种选择。
  • @ShadowRanger。我确实指出,鉴于内部实现几乎相同,这两种类型都比另一种更合理。我猜这里的具体选择在这一点上比任何具体的理由都更具历史意义。
  • 我最终弄清楚了为什么从 CPython 的角度来看list 是首选:没有方便/有效的方法来修剪 tuplefirst, *rest, last = x 的实现大致是 it = iter(x)first = next(it)rest = list(it)last = rest[-1]del rest[-1:](使用引用计数优化,所以 del rest[-1:] 实际上是免费的)。最后一步不能合法地使用tuple 完成,而不会永久浪费额外的内存(甚至比tuple 的生命周期还要长,因为小的tuples 有一个可以继续持有内存的空闲列表)。
  • 这对于函数来说不是问题,因为*args 后面永远不会有额外的位置参数,因此它可以无条件地收集所有剩余的参数,并且永远不需要再次修剪它们。所以虽然first, *rest = x 并没有真正从listiness 中受益,但*rest, last = x 可以。
猜你喜欢
  • 1970-01-01
  • 2019-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-30
  • 1970-01-01
  • 2010-10-27
  • 1970-01-01
相关资源
最近更新 更多