Python3.5元组推导式真的这么有限吗?

4

我非常喜欢Python3.5新增的元组推导式:

In [128]: *(x for x in range(5)),
Out[128]: (0, 1, 2, 3, 4)

然而,当我直接尝试返回元组推导时,我遇到了一个错误:
In [133]: def testFunc():
     ...:     return *(x for x in range(5)),
     ...: 
  File "<ipython-input-133-e6dd0ba638b7>", line 2
    return *(x for x in range(5)),
           ^
SyntaxError: invalid syntax    

这只是一个小的不方便,因为我可以将元组推导分配给变量并返回变量。但是,如果我尝试在字典推导中放置元组推导,我会得到相同的错误:

In [130]: {idx: *(x for x in range(5)), for idx in range(5)}
  File "<ipython-input-130-3e9a3eee879c>", line 1
    {idx: *(x for x in range(5)), for idx in range(5)}
          ^
SyntaxError: invalid syntax

我感觉这可能是一个问题,因为在某些情况下,推导式对性能非常重要。

在这些情况下,我没有使用字典和列表推导式的问题。除了这种情况外,还有多少其他情况元组推导式不能正常工作?或者我使用方法不正确?

这让我想知道这个方法的意义是什么,如果它的使用范围如此有限,或者是我做错了什么?如果我没有做错什么,那么创建一个元组的最快/最符合Python风格的方法是什么,以便像列表和字典推导式一样灵活使用?


14
那不是一个元组推导,而是一个生成器表达式。 - miradulo
7
* 不是生成器表达式的一部分!它是展开符号。 - MisterMiyagi
2
你可以添加括号:return (*(...),),但是你应该使用 tuple(...) - tobias_k
2
星号解包在推导式中曾经被明确考虑并被拒绝:详见PEP 448。但是,return出现语法错误让我感到惊讶;这可能只是一个疏忽。 - Mark Dickinson
2个回答

11

简而言之:如果你想要一个元组,将生成器表达式传递给tuple

{idx: tuple(x for x in range(5)) for idx in range(5)}
Python中没有"元组推导"。以下是示例代码:
x for x in range(5)

生成器表达式是一种表达式。用括号将其括起来只是为了将其与其他元素分开。这与 (a + b) * c 相同,它也不涉及元组。

* 符号用于迭代器打包/解包。生成器表达式恰好是可迭代的,因此可以对其进行解包。但是,必须有一些东西来将可迭代对象 解包 成其他东西。例如,也可以将列表解包为赋值的元素:

*[1, 2]                         # illegal - nothing to unpack into
a, b, c, d = *[1, 2], 3, 4      # legal - unpack into assignment tuple

现在,使用*<iterable>,*展开与,元组字面量结合在一起。然而,这种方法并不适用于所有情况 - 分隔元素可能优先于创建元组。例如,在[*(1, 2), 3]中,最后一个,是分隔符,而在[(*(1, 2), 3)]中,它则创建了一个元组。

在字典中,,是模棱两可的,因为它用于分隔元素。比较{1: 1, 2: 2}{1: 2,3}是非法的。对于return语句,将来可能会有解决方案

如果你想要一个元组,无论何时都应该使用()以避免歧义 - 即使 Python 可以处理它,否则对于人类来说很难解析。

当你的源是一个大语句,如生成器表达式时,我建议明确转换为元组。比较以下两个版本的代码,它们都是有效的,但哪个更易读?

{idx: tuple(x for x in range(5)) for idx in range(5)}
{idx: (*(x for x in range(5)),) for idx in range(5)}

注意,列表和字典推导式也类似地工作-实际上它们就像将生成器表达式传递给listsetdict一样。它们主要用于避免在全局命名空间中查找listsetdict


我觉得这可能是一个问题,因为在某些情况下,推导式对性能非常重要。

在底层,生成器表达式和列表/字典/集合推导式都会创建一个短暂的函数。除非您已经对其进行了分析和测试,否则不应该依赖推导式进行性能优化。默认情况下,使用对于您的用例来说最易读的方式。

dis.dis("""[a for a in (1, 2, 3)]""")
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10f730ed0, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_CONST               5 ((1, 2, 3))
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

@jpp 是的,反汇编显示了“MAKE_FUNCTION”。列表推导式会消耗中间生成器,因此您永远无法访问它。 - MisterMiyagi
1
@jpp 你说得对,它不是用于推导式的生成器函数,而是一个普通的函数。这种抽象是有缺陷的,但是这个函数可以转换成生成器函数:https://dev59.com/dlsW5IYBdhLWcg3wZWfj#50214024 请注意,我并不是说列表推导比生成器表达式+列表慢。虽然它们可能比其他方法慢。在我的测试中,使用a = [];a.extend(map(func, range(10000)))[func(i) for i in range(10000)]更快。 - MisterMiyagi
@MarkDickinson 在Python中,“,”是创建元组还是分离元素存在一些歧义。比较“[(1, 2), 3]”与“[((1, 2), 3)]”。同样,写成“{1: 2,3}”是不合法的。请注意,“return (*(1, 2),)”是有效的写法。 - MisterMiyagi
1
@MisterMiyagi:是的,但我看不出为什么要将return *(1, 2), 3变成非法的,因为同样的理由也适用于x = *(1, 2), 3(这是合法的)。在return中没有歧义,并且PEP 448的实现者似乎至少没有考虑到这种情况。 - Mark Dickinson
1
@MisterMiyagi:顺便说一句,事实证明去除限制很容易:https://github.com/python/cpython/pull/8941 - Mark Dickinson
显示剩余2条评论

2
将一个生成器表达式传递到`tuple()`构造函数中,因为不存在元组推导
{idx: tuple(x for x in range(5)) for idx in range(5)}

元组推导不存在,但尽管列表推导存在([... for ... in ...]),它们与以下语法类似:list(... for ... in ...)
列表推导式实际上比将生成器表达式传递给构造函数更快,因为在Python中执行函数是昂贵的。

@jpp 我承认我不确定它们是否如此相似,但是我读到Martijn Peters发表的这篇文章声称它们的差异可以忽略不计。你所说的生成器表达式使用"__next__ calls internally"是什么意思?是list()构造函数将调用iter()来将可迭代对象转换为迭代器,然后调用next()直到收到StopIteration。相比之下,列表推导是内置语法,因此它将被优化。 - Joe Iddon
“Syntactic sugar”(语法糖)意味着除了提高可读性之外,与原来的代码完全相同。Martijn在2013年发表了评论,我不确定它是否适用于3.6+版本。事实上,这就是我不喜欢这些旧评论的原因之一。随着事物的变化,它们无法被评估/编辑/更新。当然,它们并不相同,这可以从性能差异中看出。请参见理解Python中的生成器以了解我所说的“next”。 - jpp
当然,对于d.__getitem__d[](简单的例子),性能可能不同,但我称其为语法糖,因为这只是O(1)的差异。[x for i in y]list(x for i in y)不属于同一类别。 - jpp
2
@jpp 是的,我说它是语法糖是错误的,但它很少会表现出与实际不同的行为。 - Joe Iddon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接