为什么 Python 的列表推导式不复制参数,这样实际对象就不会被改变?

发布于 2024-09-15 01:38:15 字数 413 浏览 6 评论 0原文

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

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

为什么 d 不被复制,然后复制的词法范围版本不被复制突变(然后丢失)?列表理解的要点似乎应该是返回所需的列表,而不是返回列表并在幕后默默地改变一些其他对象。 d 的破坏有些隐式,这看起来不符合 Python 风格。这有一个好的用例吗?

为什么列表组合的行为与 for 循环完全相同,而不是更像函数(来自函数式语言,具有本地范围)?

Maybe I've been drinking too much of the functional programming Kool Aid, but this behavior of list comprehensions seems like a bad design choice:

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

Why is d not copied, and then the copied lexically-scoped version not mutated (and then lost)? The point of list comprehensions seems like it should be to return the desired list, not return a list and silently mutate some other object behind the scenes. The destruction of d is somewhat implicit, which seems unPythonic. Is there a good use case for this?

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(14

苹果你个爱泡泡 2024-09-22 01:38:15

Python 从不进行复制,除非你特别要求它进行复制。这是一个非常简单、清晰且完全可以理解的规则。给它添加例外和区别,例如“除非在列表推导式中出现以下情况……”,将是完全愚蠢的:如果Python的设计曾经由具有如此疯狂想法的人管理,那么Python将是一个病态的人,扭曲的,半残破的语言不值得学习。感谢您让我再次感到高兴,因为我意识到事实并非如此!

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

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

如果您超级热衷于在单个表达式中包含所有内容,则可以这样做,尽管它可能不是人们所说的完全“干净”的代码:

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

即,当人们确实想将赋值折叠到列表理解中时使用的常用技巧(添加一个 for 子句,其中“控制变量”是您要分配的名称,并且“循环”是在您要分配的值的单项序列上)。

函数式语言从不改变数据,因此它们也不复制(也不需要)。 Python 不是一种函数式语言,但是当然,您可以在 Python 中以“函数式方式”做很多事情,而且通常这是一种更好的方式。例如,对列表理解的更好替代(保证具有相同的结果并且不影响d,并且大大更快、更简洁,和清洁工):(

d[::-1]

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

Python never does copy unless you specifically ask it to do a copy. This is a perfectly simple, clear, and totally understandable rule. Putting exceptions and distinguos on it, such as "except under the following circumstances within a list comprehension...", would be utter folly: if Python's design had ever been under the management of somebody with such crazy ideas, Python would be a sick, contorted, half-broken language not worth learning. Thanks for making me happy all over again in the realization that is is definitely not the case!

You want a copy? Make a copy! That's always the solution in Python when you prefer a copy's overhead because you need to perform some changes that must not be reflected in the original. That is, in a clean approach, you'd do

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

If you're super-keen to have everything within a single expression, you can, though it's possibly not code one would call exactly "clean":

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

i.e., the usual trick one uses when one would really like to fold an assignment into a list comprehension (add a for clause, with the "control variable" being the name you want to assign to, and the "loop" is over a single-item sequence of the value you want to assign).

Functional languages never mutate data, therefore they don't make copies either (nor do they need to). Python is not a functional language, but of course there's a lot of things you can do in Python "the functional way", and often it's a better way. For example, a much better replacement for your list comprehension (guaranteed to have identical results and not affect d, and vastly faster, more concise, and cleaner):

d[::-1]

(AKA "the Martian Smiley", per my wife Anna;-). Slicing (not slice assignment, which is a different operation) always perform a copy in core Python (language and standard library), though of course not necessarily in independently developed third party modules like the popular numpy (which prefers to see a slice as a "view" on the original numpy.array).

九八野马 2024-09-22 01:38:15

在此表达式中:

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

您希望隐式复制或限定什么变量?这里唯一在推导式中具有任何特殊状态的变量是 _,它不是您想要保护的变量。

我不明白如何提供列表推导式语义,以某种方式识别涉及的所有可变变量,并以某种方式隐式复制它们。或者知道 .pop() 改变了它的对象?

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

In this expression:

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

what variable do you want to be implicitly copied or scoped? The only variable here with any kind of special status in the comprehension is _, which isn't the one you want protected.

I don't see how you could give list comprehensions semantics that could somehow identify all of the mutable variables involved, and somehow implicitly copy them. Or to know that .pop() changes its object?

You mention functional languages, but they accomplish what you want by making all variables immutable. Python simply isn't designed that way.

梦幻的味道 2024-09-22 01:38:15

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

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

Why should it create a (possibly very expensive) copy, when idiomatic code won't have side effects anyway? And why should the (rare, but existing) use cases where side effects are desired (and ok) be prohibited?

Python is first and foremost an imperative language. Mutable state is not only permitted, but essential - yeah, list comprehensions are intended to be pure, but if that was enforced, it would be asynch with the semantics of the rest of the language. So d.pop() mutates d, but only if it's not in a list comprehension and if the stars are right? That would be pointless. You're free (and supposed) not to make use of it, but nobody's going to set more rules in stone and make the feature more complex - idiomatic code (and that's the only code anyone should care about ;) ) doesn't need such a rule. It does so anyway, and does otherwise if needed.

孤千羽 2024-09-22 01:38:15

d 不会被复制,因为您没有复制它,并且列表是可变的,并且 pop 通过契约操作列表。

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

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

d isn't copied because you didn't copy it and lists are mutable and pop contractually manipulates the list.

If you'd used a tuple, it would not have mutated:

>>> x = (1, 2, 3, 4)
>>> type(x)
<type 'tuple'>
>>> x.pop()
AttributeError: 'tuple' object has no attribute 'pop'
绮烟 2024-09-22 01:38:15

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

因为它最不令人惊讶。

而不是表现得更像函数
(具有本地范围)?

你在说什么?函数可以改变它们的参数:

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

我完全没有看到任何不一致之处。

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

Why is it advantageous to have list
comps behave exactly like for loops,

Because it's least surprising.

rather than behave more like functions
(with local scope)?

What are you talking about? Functions can mutate their arguments:

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

I see no inconsistency at all.

What you seem not to recognize is that Python is not a functional language. It's an imperative language that happens to have a few functional-like features. Python allows objects to be mutable. If you don't want them mutated, then just don't call methods like list.pop that are documented to mutate them.

以往的大感动 2024-09-22 01:38:15

您似乎误解了函数:

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

将具有与 This 完全相同的效果

(lst.pop() for _ in range(len(lst)))

,因为 lst 不是“the”列表,而是对它的引用。当您传递该引用时,它仍然指向同一个列表。如果您想要复制列表,只需使用lst[:]即可。如果您还想复制其内容,请使用 copy 模块中的 copy.deepcopy

You seem to be misunderstanding functions:

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

will have exactly the same effect as

(lst.pop() for _ in range(len(lst)))

This is because lst is not 'the' list but a reference to it. When you pass that reference around, it stays pointing to the same list. If you want the list copied, simply use lst[:]. If you want to copy its contents as well, use copy.deepcopy from the copy module.

决绝 2024-09-22 01:38:15

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

Python 与任何值得使用的编程语言一样,完全按照您的指示执行,不多也不少。如果你想让它做别的事情,那就告诉它做别的事情。

Of course there is (e.g., queue processing). But clearly what you have shown isn't one.

Python, like any programming language worth using, does exactly what you tell it to, no more and no less. If you want it to do something else, then tell it to do something else.

秋心╮凉 2024-09-22 01:38:15

Python 不是一种函数式语言,而且永远也不会。因此,当您使用列表理解时,您可以更改不相关数据结构的状态。这是无法合理阻止的,您所描述的措施只会对您强调的特定情况有所帮助。

一般来说,由使用列表理解的人来编写相当容易理解并且尽可能没有副作用的代码。我认为您发布的代码是糟糕的编程风格,并且是在 list.reverse 存在时创建反向列表的愚蠢方法。

不过,我想如果您在该示例中弹出的列表是一个队列,可以通过队列处理代码(即比 d.pop() 复杂得多)或通过另一个线程,那么代码是一种合理的做事方式。虽然我真的认为它应该是一个循环而不是列表理解。

Python is not a functional language and never will be. Therefore, when you use a list comprehension, you can alter the state of unrelated data structures. This cannot be reasonably prevented, and measures such as the one you describe would only help the particular case you highlighted.

In general, it's up to the person using a list comprehension to write code that is fairly easy to understand and as free of side-effects as possible. I consider the code you posted to be bad programming style and a dumb way to create a reversed list when list.reverse exists.

Though, I suppose if the list you're popping from in that example is a queue that can be added to by the queue processing code (i.e. something much more complicated than d.pop()) or by another thread, then the code is sort of a reasonable way to do things. Though I really think it ought to be a loop and not a list comprehension.

你在我安 2024-09-22 01:38:15

您是说您希望方法根据执行上下文而表现不同吗?听起来对我来说真的很危险。

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

Are you saying that you want methods to behave differently depending on the execution context? Sounds really dangerous to me.

It's a good thing that a method called on a Python object will always do the same thing - I'd be worried about using a language where calling a method inside some kind of syntactic construction caused it to behave differently.

栀子花开つ 2024-09-22 01:38:15

使用列表推导式时,总有一种方法可以改变列表。但如果您想要的话,您也可以改变 list 。例如,在您的情况下:

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

都会为您提供 list 的反向副本。 While

d.reverse()

会将 list 反转到位。

There's always a way to not mutate the list when using list comprehensions. But you can mutate the list too, if that's what you want. In your case, for example:

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

will all give you a reversed copy of the list. While

d.reverse()

will reverse the list in place.

べ映画 2024-09-22 01:38:15

为什么 d 没有被复制,然后复制的词法范围版本没有发生变化(然后丢失)?

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

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

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


为什么让列表组件的行为与 for 循环完全相同,而不是更像函数(具有本地作用域)?

  1. 因为它创建了简洁、可读的代码。
  2. 正如其他人所指出的,函数并不像您想象的那样工作。他们也不做这种“词法范围副本”的事情。我认为您对本地分配感到困惑。

我建议阅读这里的文章: http://www.cafepy.com/ Article/python_types_and_objects/python_types_and_objects.html

它们非常详细地介绍了 python 的工作原理。

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

Because python is an object oriented programming language and doing so would be an incredibly bad idea. Everything is an object.

What makes you think it's possible to create "lexically scoped copies" of arbitrary objects?

Being able to call pop on an object doesn't mean it's possible to copy it. It might access a file handle, a network socket or an instruction queue for a space probe orbiting Saturn.


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

  1. Because it creates concise, readable code.
  2. As everyone else has pointed out, functions don't work in the way you appear to think they do. They don't do this "lexically scoped copies" thing either. I think you're getting confused with local assignment.

I recommend having a read of the articles here: http://www.cafepy.com/article/python_types_and_objects/python_types_and_objects.html

They are very informative about how python works.

撩发小公举 2024-09-22 01:38:15

如果我考虑一下你的“列表理解”,它本身就是“unpythonic”。
您通过 d.pop() 引用 d 并且实际上没有引用“列表理解”中的列表。
因此,实际上,您在使用模拟变量“_”的简单 for 循环中滥用了列表理解,而您并未将其用于此表达式中实际执行或收集的操作:“d.pop()”。
pop() 方法应用于 d。并且与 _ 和 range(len(d)) 无关 - 它只是使用 d 的长度创建另一个列表。
列表上的方法会改变列表本身。因此,通过应用此方法来改变 d 是“合乎逻辑的”。

正如 Alex Martelli 回答的那样,d[::-1] 以“pythonic”方式完成了该表达式应该做的事情。

If I think about your 'list comprehension', it itself is 'unpythonic'.
You refer by the d.pop() to d and you have actually no reference to the list in the 'list comprehension'.
So actually you are misusing the list comprehension for a simple for-loop using a mock variable '_' which you don't use for what you actually do or collect in this expression: 'd.pop()'.
The pop() method is applied to d. And has nothing to do with _ nor with range(len(d)) - which creates just another list using the length of d.
Methods on lists mutate the list itself. So it is 'logical' that d gets altered by application of this method.

As Alex Martelli answered, d[::-1] does what this expression should do in a 'pythonic' way.

甜是你 2024-09-22 01:38:15

我不确定你在问什么。也许您会问 d.pop() 是否应该返回一个副本而不是改变自身。 (这与列表推导式完全无关。)答案是否定的,当然不是:这会将其从 O(1) 操作转变为 O(n) 操作,这将是一个灾难性的设计缺陷。

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

I'm not sure what you're asking. Maybe you're asking if d.pop() should return a copy instead of mutating itself. (That has nothing at all to do with list comprehensions.) The answer to that is no, of course not: that would turn it from an O(1) operation to O(n), which would be a catastrophic design flaw.

As for list comprehensions, nothing stops expressions within them from calling functions with side-effects. It's not the fault of list comprehensions if you misuse them. It's not the job of language design to forcefully prevent programmers from doing confusing things.

坐在坟头思考人生 2024-09-22 01:38:15

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 是一种命令式语言”的答案在这种情况下并不完全正确。

dan04 has the right answer, and for comparison, here's a little Haskell...

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

Here you have a side effect (printing) right in the middle of a list comprehension in Haskell. Haskell is lazy, so you have to explicitly run it with sequence_:

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

But that's practically the same as Python's

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

Except that Haskell wraps the _ = list... idiom in function named sequence_.

List comprehensions don't have anything to do with preventing side effects. It's just unexpected to see them there. And you can hardly get more 'functional' than Haskell, so the answer "Python is an imperative language" isn't quite right in this context.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文