为什么Python的列表推导不能复制参数,所以实际的对象不能变异?

时间:2010-08-21 20:22:39

标签: python functional-programming list-comprehension

也许我一直在喝太多的函数式编程Kool Aid,但列表推导的这种行为似乎是一个糟糕的设计选择:

>>> d = [1, 2, 3, 4, 5]
>>> [d.pop() for _ in range(len(d))]
[5, 4, 3, 2, 1]
>>> d
[]

为什么不复制d,然后复制的词法范围版本没有变异(然后丢失)?列表推导的重点似乎应该是返回所需的列表,而不是返回列表并默默地改变幕后的其他对象。 d的破坏有些含蓄,似乎是非战争的。这有一个很好的用例吗?

为什么让list comps的行为与for循环完全相同,而不是像函数一样(来自函数式语言,具有局部范围)?

14 个答案:

答案 0 :(得分:21)

除非你特别要求它复制,否则Python永远不会复制。这是一个非常简单,清晰且完全可以理解的规则。对它进行例外和区分,例如“除了在列表理解中的以下情况......”之外,将是完全愚蠢的:如果Python的设计曾经由一些有这种疯狂想法的人管理,那么Python会生病,扭曲,半破的语言不值得学习。感谢让我再次感到高兴,实现绝对是的情况!

你想要一份副本吗? 制作副本!当您更喜欢副本的开销时,始终是Python中的解决方案,因为您需要执行一些不能反映在原始文件中的更改。也就是说,在 clean 方法中,你会做

dcopy = list(d)
[dcopy.pop() for _ in range(len(d))]

如果你非常热衷于在一个表达式中包含所有内容,那么你可以,虽然它可能不是代码,但可以称之为“干净”:

[dcopy.pop() for dcopy in [list(d)] for _ in range(len(d))]

即,当人们真的想将一个赋值折叠成一个列表理解时添加for子句,其中“控制变量”是你想要分配的名称,并且“loop”在您要分配的值的单项序列上。

功能语言从不改变数据,因此它们也不会复制(也不需要)。 Python不是一种函数式语言,但当然你可以用Python“功能性方式”做很多事情,而且往往是一种更好的方式。例如,很多可以更好地替换列表理解(保证结果相同但不影响d极大更快,更简洁,更清晰) :

d[::-1]

(AKA“火星笑脸”,根据我的妻子安娜;-)。切片(不是切片赋值,这是一个不同的操作)总是在核心Python(语言和标准库)中执行复制,但当然不一定在独立开发的第三方模块中,如流行的{{1 (更喜欢在原始numpy上将切片视为“视图”)。

答案 1 :(得分:7)

在这个表达式中:

[d.pop() for _ in range(len(d))]

您希望隐式复制或限定哪个变量?理解中具有任何特殊状态的唯一变量是_,这不是您想要保护的那个。

我不知道你怎么能给出列表推导语义,它可以某种方式识别所涉及的所有可变变量,并以某种方式隐式地复制它们。或者知道.pop()改变了它的对象?

您提到了函数式语言,但它们通过使所有变量不可变来实现您想要的功能。 Python根本就不是这样设计的。

答案 2 :(得分:5)

为什么它会创建一个(可能非常昂贵的)副本,当惯用代码无论如何都没有副作用?为什么(罕见的,但现有的)用例需要副作用? (还可以)被禁止?

Python首先是一种命令式语言。可变状态不仅是允许的,而且是必不可少的 - 是的,列表推导是纯粹的,但是如果执行它,它将与语言的其余部分的语义异步。所以d.pop()会改变d,但只有当它不在列表理解中并且星星是否正确时?那毫无意义。你是自由的(并且应该)不使用它,但没有人会设置更多的规则并使功能更复杂 - 惯用代码(这是任何人应该关心;)的唯一代码)不需要这样的规则。无论如何它都会这样做,如果需要的话也会这样做。

答案 3 :(得分:4)

d未被复制,因为您没有复制它,列表是可变的,pop合同操纵列表。

如果您使用了元组,它就不会发生变异:

>>> x = (1, 2, 3, 4)
>>> type(x)
<type 'tuple'>
>>> x.pop()
AttributeError: 'tuple' object has no attribute 'pop'

答案 4 :(得分:4)

  

为什么列表有利   comps的行为与for循环完全相同,

因为它最不令人惊讶。

  

而不是表现得更像功能   (有本地范围)?

你正在谈论什么?函数可以改变他们的参数:

>>> def mutate(d):
...     d.pop()
... 
>>> d = [1, 2, 3, 4, 5]
>>> mutate(d)
>>> d
[1, 2, 3, 4]

我看不出任何矛盾。

你似乎没有意识到Python不是一种功能语言。这是一种命令式语言恰好具有一些类似功能的特性。 Python允许对象是可变的。如果您不希望它们发生变异,那么就不要调用记录的list.pop之类的方法来改变它们。

答案 5 :(得分:3)

你似乎误解了功能:

def fun(lst):
    for _ in range(len(lst)):
        lst.pop()

具有完全相同的效果
(lst.pop() for _ in range(len(lst)))

这是因为lst不是'the'列表而是对它的引用。当您传递该引用时,它将保持指向同一列表。如果要复制列表,只需使用lst[:]。如果您还要复制其内容,请使用copy.deepcopy模块中的copy

答案 6 :(得分:2)

当然有(例如,队列处理)。但显然你所展示的不是一个。

Python,就像任何值得使用的编程语言一样,完全按照你所说的去做,不多也不少。如果你想要它做其他事情,那就告诉它做别的事。

答案 7 :(得分:2)

Python不是一种功能语言,永远不会。因此,当您使用列表推导时,您可以更改不相关的数据结构的状态。这是无法合理防止的,而您所描述的措施只会对您突出显示的特定情况有所帮助。

通常,使用列表推导的人可以编写相当容易理解并且尽可能没有副作用的代码。我认为您发布的代码是错误的编程风格,并且在list.reverse存在时创建反向列表的方式很愚蠢。

尽管如此,我想如果你在该示例中弹出的列表是一个可以由队列处理代码添加的队列(即比d.pop()更复杂的东西)或另一个线程,那么代码是一种合理的做事方式。虽然我真的认为它应该是一个循环而不是列表理解。

答案 8 :(得分:2)

您是否希望根据执行上下文,方法的行为方式不同?听起来真的对我很危险。

调用Python对象的方法总是会做同样的事情是一件好事 - 我担心使用一种语言,在某种句法结构中调用一个方法会导致它的行为不同。

答案 9 :(得分:2)

使用列表推导时,总是有办法改变list。但你也可以改变list,如果这是你想要的。在您的情况下,例如:

c = [a for a in reversed(d)]
c = d[::-1]
c = [d[a] for a in xrange(len(d)-1, -1, -1)]

将全部为您提供list的反向副本。而

d.reverse()

将反转list

答案 10 :(得分:2)

Why is d not copied, and then the copied lexically-scoped version not mutated (and then lost)?

因为python是一种面向对象的编程语言,所以这样做是一个非常糟糕的主意。 一切都是对象

是什么让你认为可以创建任意对象的“词法范围副本”?

能够在对象上调用pop并不意味着可以复制它。它可能会访问绕土星运行的空间探测器的文件句柄,网络套接字或指令队列。


Why is it advantageous to have list comps behave exactly like for loops, rather than behave more like functions (with local scope)?

  1. 因为它创建了简洁易读的代码。
  2. 正如其他人所指出的那样,功能不会像你认为的那样工作。他们也不会做这个“词法范围副本”的事情。我认为你对当地的任务感到困惑。

  3. 我建议您阅读这些文章:http://www.cafepy.com/article/python_types_and_objects/python_types_and_objects.html

    他们对python如何运作非常有用。

答案 11 :(得分:2)

如果我考虑一下你的列表理解,那么它本身就是“unpythonic&#39;”。 你通过d.pop()来引用d,实际上你没有引用列表理解中的列表&#39;。 所以实际上你是使用模拟变量&#39; _&#39;来滥用列表理解来进行简单的for循环。您不能用于实际执行或收集的内容:&#39; d.pop()&#39;。 pop()方法应用于d。并且与_无关,也与范围(len(d))无关 - 它使用d的长度创建另一个列表。 列表上的方法会改变列表本身。所以这是“合乎逻辑的”。通过应用这种方法来改变d。

正如Alex Martelli回答的那样,d [:: - 1]做了这个表达应该在&#39; pythonic&#39;方式。

答案 12 :(得分:1)

我不确定你在问什么。也许你在问d.pop()是否应该返回副本而不是变异本身。 (这与列表推导没有任何关系。)答案当然不是:那会将它从O(1)操作变为O(n),这将是一个灾难性的设计缺陷。 / p>

对于列表推导,没有什么可以阻止其中的表达式调用具有副作用的函数。如果你滥用它们,那不是列表推导的错。强制阻止程序员做出令人困惑的事情并不是语言设计的工作。

答案 13 :(得分:1)

dan04有正确的答案,相比之下,这里有一点Haskell ......

[print s | s <- ["foo", "bar", "baz"]]

这里有一个副作用(打印)正好在Haskell中的列表理解中。 Haskell很懒,所以你必须用sequence_显式运行它:

main = sequence_ [print s | s <- ["foo", "bar", "baz"]]

但这几乎与Python的

相同
_ = list(print(s) for s in ["foo", "baz", "baz"])

除了Haskell将_ = list...成语包装在名为sequence_的函数中。

列表推导与防止副作用无关。在那里看到他们是出乎意料的。而且你很难获得比Haskell更多的“功能性”,因此在这种背景下,“Python是一种命令式语言”的答案并不完全正确。