为什么 Python 中没有元组推导式?

521

众所周知,Python中有列表推导式,例如

[i for i in [1, 2, 3, 4]]

还有字典推导式,例如

{i:j for i, j in {1: 'a', 2: 'b'}.items()}

但是

(i for i in (1, 2, 3))

将最终结果放在生成器中,而不是tuple推导式中。为什么呢?

我猜这是因为tuple是不可变的,但这似乎不是答案。


33
还有一个集合推导式 —— 它看起来很像字典推导式... - mgilson
2
仅为了记录,这个问题正在Python聊天室中讨论。 - Inbar Rose
1
显然是有的。https://dev59.com/-63la4cB1Zd3GeqPPJZ-#51811147 - Programmer S
13个回答

755
可以使用生成器表达式:
tuple(i for i in (1, 2, 3))

但是括号已经被用来表示生成器表达式了。


36
通过这个论点,我们可以说列表推导式也是不必要的: list(i for i in (1,2,3))。我真的认为这只是因为没有一个清晰的语法来表示它(或者至少没有人想到过)。 - mgilson
113
列表、集合或字典推导式只是一种语法糖,用于使用生成器表达式输出特定类型。例如,list(i for i in (1,2,3)) 是一个生成器表达式,输出一个列表;set(i for i in (1,2,3)) 输出一个集合。这是否意味着不需要推导式的语法呢?也许不是,但它非常方便。对于你需要元组的罕见情况,生成器表达式可以胜任,而且清晰明了,不需要发明另一个括号或方括号。 - Martijn Pieters
31
答案显然是因为元组语法和括号具有歧义。 - Charles Salvia
44
如果您关心性能,那么使用列表推导式和使用构造函数+生成器之间的差异不仅是微妙的。与将生成器传递给构造函数相比,列表推导式构建速度更快。在后一种情况下,您正在创建和执行函数,而在Python中,函数具有昂贵的开销。[thing for thing in things] 构造列表比 list(thing for thing in things) 更快。元组推导式并非毫无用处;tuple(thing for thing in things) 存在延迟问题,而 tuple([thing for thing in things]) 可能存在内存问题。 - Justin Turner Arthur
19
@MartijnPieters,你能否重新表述一下“列表、集合或字典推导式只是使用生成器表达式的语法糖”?这引起了混淆,因为人们将它们视为实现同样目的的等效手段。从技术上讲,它并不是语法糖,因为这些过程实际上是不同的,即使最终产品是相同的。 - jpp
显示剩余5条评论

106

Python核心开发人员之一Raymond Hettinger在最近的推文中对元组发表了以下看法:

#python提示:通常,列表用于循环;元组用于结构体。列表是同构的;元组是异构的。列表用于可变长度。

我认为这个观点支持这样一个想法:如果序列中的项目足够相关以至于可以通过生成器生成,那么它应该是一个列表。尽管元组是可迭代的,并且看起来像一个不可变的列表,但它实际上是Python中C结构体的等价物:

struct {
    int a;
    char b;
    float c;
} foo;

struct foo x = { 3, 'g', 5.9 };

在Python中成为

x = (3, 'g', 5.9)

46
不变性是一个很重要的属性,通常使用元组而不是列表可以保持这个属性。例如,如果你有一个包含5个数字的列表,希望将它们作为字典的键来使用,那么使用元组是更好的选择。 - pavon
2
这是Raymond Hettinger的一个好建议。我仍然认为使用生成器与元组构造函数有用例,例如通过迭代要转换为元组记录的属性来将另一个结构(可能更大)解包为较小的结构。 - dave
2
@dave 在这种情况下,你可能只需要使用 operator.itemgetter - chepner
@chepner,我明白了。这非常接近我的意思。它确实返回一个可调用对象,因此如果我只需要执行一次,那么与直接使用tuple(obj[item] for item in items)相比,我不认为有太大的优势。在我的情况下,我将其嵌入到列表推导式中,以创建元组记录列表。如果我需要在代码中反复执行此操作,则itemgetter看起来很棒。也许无论如何,itemgetter都更符合惯用法? - dave
3
我认为frozenset和set之间的关系类似于tuple和list之间的关系。这与异构性无关,更多的是关于不可变性 - frozensets和tuples可以作为字典的键,而由于其可变性,列表和集合则不能。 - polyglot
2
还有一种常见情况,你使用生成器来生成类似结构体的东西:当你处理文本记录(如CSV)时。通常会写成line_values = tuple(int(x.trim()) for x in line.split(','))。正如其他人所指出的,这里使用元组构造函数而不是推导式会影响性能,并且解析此类大型数据集是您真正关心性能的情况。 - Tom

104

自 Python 3.5 开始, 您还可以使用星号 * 展开语法来展开生成器表达式:

*(x for x in range(10)),

4
很好(而且它有效),但我找不到任何文件记录它!你有链接吗? - felixphew
26
注意:这个实现细节基本上与执行 tuple(list(x for x in range(10))) 相同(代码路径是相同的,两者都创建一个列表,唯一的区别是最后一步是从列表中创建一个元组,并在需要元组输出时丢弃列表)。这意味着你实际上并没有避免使用一对临时变量。 - ShadowRanger
12
根据@ShadowRanger的评论,进一步阐述一下,在这个问题中,他们展示了使用星号打包和元组字面量语法实际上比将生成器表达式传递给元组构造函数要慢得多。 - Lucubrator
3
我正在尝试在Python 3.7.3中运行此代码,但是*(x for x in range(10))无法正常工作。我得到了SyntaxError: can't use starred expression here的错误提示。然而,tuple(x for x in range(10))可以正常工作。 - Ryan H.
22
@RyanH,你需要在末尾加上一个逗号。 - czheo
显示剩余3条评论

78

36
注意:列表推导式元组 需要基于最终 元组列表 的大小来计算峰值内存使用量。而 生成器表达式元组,虽然比较慢,但意味着您只需支付最终 元组 的费用,没有临时的 列表(生成器表达式本身占据大致固定的内存)。通常情况下这不重要,但当涉及到的大小特别大时,就可能变得重要。 - ShadowRanger
1
非常有用的信息。在这种情况下,从生成器中获取元组可能不是最好的选择。我认为tuple([i for i in range(1000)])在可读性和速度方面是最好的选择。当然,对于更小/更大/不同的数据集,时间可能会有所不同。 - jamylak
2
当我在使用列表推导式生成元组和使用生成器生成元组时,使用更大的数据(比如 range(1_000_000))时你会发现生成器生成的元组所需时间更短。虽然这个差别不是很明显,但在处理更大的数据时,你将节省空间和时间。 - Utsav Patel

37

理解工作是通过循环或迭代遍历项目并将它们分配到一个容器中完成的,元组无法接收分配。

一旦创建了元组,就不能向其附加、扩展或分配。修改元组的唯一方法是如果其中一个对象本身可以被赋值(是非元组容器)。因为元组只持有对该类型对象的引用。

另外,元组有自己的构造函数tuple(),您可以将任何迭代器传递给它。这意味着要创建一个元组,您可以执行以下操作:

tuple(i for i in (1,2,3))

10
在某些方面,我同意(关于不必要因为列表就可以实现),但在其他方面,我不同意(关于推理是因为它是不可变的)。在某些方面,为不可变对象创建一个理解更有意义。谁会使用“lst = [x for x in ...]; x.append()” 的方法呢? - mgilson
@mgilson 我不确定这与我所说的有何关联? - Inbar Rose
3
如果一个元组是不可变的,那么其底层实现就无法“生成”一个元组(“生成”意味着一次构建一个元素)。不可变的意思是你不能通过改变一个有3个元素的元组来构建一个有4个元素的元组。相反,你可以通过先构建一个列表(用于构建序列),然后在最后一步构建元组,并且丢弃列表来实现元组的“生成”。这种语言反映了这种现实。把元组想象成C结构体。 - Scott
2
虽然通过理解的语法糖对元组起作用是合理的,因为在返回推导表达式之前无法使用该元组。实际上它并不像可变对象,而元组推导表达式的行为更像字符串的追加操作。 - uchuugaka
(1,2,3)不够简单时,你会怎么做? - Ryan

15

我最好的猜测是他们用完了括号,而且认为添加一个“丑陋”的语法不够有用...


1
角括号未使用。 - uchuugaka
不,那是非常不同的。语言语法对这些标记非常具体。它们有非常清晰的语义和词汇范围,如果应用于其他括号的新标记,这将是无歧义的(或者几乎足以通过微小的更改使其工作)。圆括号已经用于划分许多不同事物的范围,因此应该非常容易。只要有人决定这样做就好了。老实说,集合也需要一些文字上的关注和推导应该获得符号。 - uchuugaka
7
值得注意的是, {*()} 虽然丑陋,但作为空集合文字很有效! - user4698348
4
嗯,从审美角度来看,我认为我更偏向于使用 set() :) - mgilson
4
是的,这就是关键所在;展开通用化使得空的“集合字面量”成为可能。请注意,{*[]} 严格劣于其他选项;空字符串和空元组是不可变的单例,因此构建空集不需要临时变量。相比之下,空列表不是单例,因此你实际上需要构建它,使用它来构建集合,然后销毁它,失去任何微不足道的性能优势。 - ShadowRanger
显示剩余7条评论

14

元组无法像列表一样有效地进行追加。

因此,元组推导需要在内部使用列表,然后转换为元组。

这与您现在所做的相同:tuple( [ comprehension ] )


3

圆括号不会创建一个元组。也就是说,one = (two) 不是一个元组。唯一的解决方法是使用 one = (two,) 或者 one = tuple(two)。因此,一个解决方案如下:

tuple(i for i in myothertupleorlistordict) 

one = (two,)one = tuple(two) 的值不同。tuple 函数的参数必须是一个迭代器。one = (two,) 等价于 one = tuple(i for i in two)one = tuple((two,)),以及 one = tuple([two]) - markusk

1
我认为这只是出于清晰的考虑,我们不想用太多不同的符号来混淆语言。此外,tuple 推导从未是必要的,可以使用列表代替,速度差异可以忽略,而字典推导与列表推导则不同。

1
“同时,元组推导从未是必要的,可以使用列表代替而几乎没有速度差异。” 使用列表而不是元组调用C++库可能会返回错误。但是将列表转换为元组并不难,只需使用 tuple(list) 即可。 - mins
1
@mins 根据时间来看,这似乎是你可以从这里 https://dev59.com/7mQn5IYBdhLWcg3wTVl0#48592299 选择的最佳选项。 - jamylak

0

因为你无法向元组中添加项目。这是如何将简单的列表推导转换为更基本的Python代码。

_list = [1,2,3,4,5]
clist = [ i*i for i in _list ]
print(clist)

clist1 = []
for i in _list:
    clist1.append(i*i)
print(clist1)

现在对于上面的示例,使用元组推导意味着将项目添加到不允许的元组中。但是,一旦准备好了,您可以使用tuple(clist1)将此列表转换为元组。


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