views:

282

answers:

13

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)?

+2  A: 

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.

Ignacio Vazquez-Abrams
It's really poor form to go around down-modding without saying why. At time of writing this, three answers have been down modded (including this answer) and there have been no explanations.
MattH
+4  A: 

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'
msw
I agree that tuples solve the problem... and not copying `d` was not because I'm unaware on copying objects in Python. I just wonder why it's advantageous to have list comps behave exactly like `for` loops, rather than behave more like functions (with local scope).
Vince
Even if it was passed to a function, it would be a reference so any methods called on it would still mutate the original object.
Skilldrick
Good question, so I hoisted it into the question body for you. In most other languages I'd be tempted to say "historical accident", but van Rossum and crew are usually more considered than that.
msw
True. And I guess Python really isn't trying to be functional. The issue though is that people constantly *talk* about list comprehensions as functional tools, but the openness for side-effects is huge.
Vince
A lot of functional languages easily allow side effects, too. All ML-derivates, for example. It's not functional in the sense that it forces you to stay pure, it's functional in that its intended and main use lies in functional programming constructs.
delnan
@Vince, why are you saying that a function wouldn't modify a list? it happens all the time. see my answer.
aaronasterling
+2  A: 

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.

Omnifarious
A down-mod with no comment. Woohoo!
Omnifarious
Sorry, the down-mod was me. The code was intentionally trivial and bad, yet you suggest list.reverse. It gave me the sense you didn't read my question - as I stated many times, I was not trying to build a list reverser, but in fact asking about Python design choices.
Vince
@Vince - I knew you weren't trying to build a list reverser, and you were asking a more general question. Really, only the first paragraph directly addressed your question. The middle paragraph was basically "Don't do that!". And the last paragraph tried to come up with a use case for why you might even _want_ to write code that looked like that. :-)
Omnifarious
+2  A: 

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.

Skilldrick
Not the methods... the object it's acting on, hence the phrase lexically-scoped.
Vince
Amen! I hate languages like MATLAB where the behavior of `f` in `x, y = f(z)` can depend on x and y.
dan04
I'd be interested to know what's incorrect about this answer...
Skilldrick
+2  A: 

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.

Chinmay Kanchi
+3  A: 

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.

aaronasterling
Functions in a *functional* programming language.
Vince
My preferred way to copy a list is to do `lst[:]`.
Skilldrick
@Vince Is lisp considered functional? in Lisp, you have to explicitly rebind a passed in argument using a `let` form to get the sort of behavior you're talking about. That's why you customarily append `!` to the end of function names that don't do this. The ability to write a function that modifies its input is a __feature__ in that it allows you to operate on very large lists that would be expensive to copy. Side effects are not bad as long as they are known and isolated.
aaronasterling
@Skilldrick, right, I always forget about that for some reason. Keep reminding me ;) It will sink in.
aaronasterling
+7  A: 

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.

Ned Batchelder
+1  A: 

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.

Glenn Maynard
+4  A: 

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.

dan04
+15  A: 

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).

Alex Martelli
This does not at all answer the OPs question and is only peripherally related.
Omnifarious
Thanks Alex, this makes sense. I see the balance now Python struck (correct me if I'm wrong): mutating objects in list comps could lead to awkward implicit code (as my example attempted to illustrate), but any implicit code dreamt up by a coder can't come close to the damage done by implicitly doing copies in some places, but not others by the core language.
Vince
Why does she call it the "Martian smiley?"
NullUserException
It's a smiley with four eyes?
Vince
@Omnifarious, peculiar, isn't it, that the OP (who should know better than you whether your assertions are valid) appears to disagree with you (I disagree with you vehemently on both counts, of course), judging from his "this makes sense" and "I see the balance". I guess I don't need to explain _why_ I totally disagree from your assertions (indeed I consider them so utterly wrong and unfounded as to border on "crazy")...
Alex Martelli
@Vince, you're most welcome! Yes, that's the gist of it: subtle behavioral differences between list comprehensions and `for` statements would be laying equally-subtle traps for the poor coders (as well as seriously impacting performance for no good reason -- hidden, implicit "invisible support" for the iffy behavior of altering objects in a listcomp would not be a _good_ reason;-). Coders _will_ code badly -- no language exists that can stop them from doing that -- but at least the language shouldn't go out of its way to support and reward bqd coding!-)
Alex Martelli
@Null, wrt the Martian Smiley: the four eyes as noticed by @Vince are part of it, so are the squared "mouth" and "forehead" given by the square brackets (as against the rounded ones that normal round parentheses depict in the usual "smiley" icon).
Alex Martelli
@Alex: No matter, I find your answers largely lacking in merit about 75% of the time (and when I find they have merit, I up-mod them). They are pontificating, preachy, obnoxious and frequently irrelevant. I guess most of the rest of the population here lacks any good taste since I see you up-modded even when you clearly completely fail to address the OPs concerns (and I do not count this case among those). In this case I erred because I read the first sentence and thought you were off on another irrelevant tangent. I ought to have read more carefully.
Omnifarious
@Omni If the OP him/herself said "Thanks Alex, this makes sense." doesn't that mean it addressed his/her concerns at some level?
NullUserException
@NullUserException: Notice both that my comment came before the OPs, and my admission of error later.
Omnifarious
@Omnifarious: your user page shows that you believe there is a bias; given that Alex is a user with a lot of karma and a Python contributor, I suspect you think my favoring of his answer is evidence of said bias. Two points though: (1) Did you ever suppose there is very little bias, but really people with more karma *do* post better answers, which gives them more karma? (2) Who the hell cares about karma? I come here to learn from those wiser than I. People come here to teach. Stackoverflow shouldn't be an unconstrained optimization of karma.
Vince
We are calling reputation "karma" now?
NullUserException
@Omnifarious, as you say, you _definitely_ "ought to have read more carefully" before criticizing and down-voting -- but clearly, despite your "can't be bothered to read" behavior, you _still_ believe you're somehow better, smarter, and more perceptive about (and against) me, than "the rest of the population" (and, I assume, the readers that made my books best-sellers, the writers who queue to have me tech-edit their books, the conference organizers who invite me to talk or keynote, etc;-). Now, does this say anything about _me_, or, rather, does this say it about **you**?-)
Alex Martelli
@Null, I've always called it "rep" (short for "reputation" of course, which is way too long a word to use often;-). I think my karma, the real one, has probably benefited more from me adopting a rescued dog and making it happy, than it has from all my technical contributions (not just those on SO) put together.
Alex Martelli
@Alex - It tells me that your self-importance is well earned. I do not think you always give bad answers, but I think you coast on your reputation, and I think other people help you do that. @Vince - It's a game, and while I enjoy helping people solve their problems, the reason I do it here rather than on IRC is the game. I think Alex's high reputation is at least partly deserved, but I also think it has a self-reinforcing effect that I do not like.
Omnifarious
@Vince - And in this case, I think his answer is better than mine. Highlighting that list comprehensions should in all ways behave like syntactic sugar for a `for` loop lest the language become an incomprehensible morass of special cases is a really good point.
Omnifarious
@Omni That is inevitable. Just look at Jon Skeet.
NullUserException
@Omni, are you considering how your assessment is affected by the fact that no more than 20 upvotes per day are counted towards rep? Yesterday for example I think I received 55 upvotes (eyeball count since of course it will be in the online SQL-queriable data dump only in September), so, unless I deserved less than 36% of those upvotes, my rep wasn't raised by any undeserved upvote -- so where does that leave your "self-reinforcing effect" apparent "certainty"?-) (Do use the SQL-readable data dump to check on whole months and many top-rep users, of course, and let us know on meta!-).
Alex Martelli
I take "Beating Dead Horses" for $400, please, Alex.
msw
@msw, you mean Omnifarious is dead?! Otherwise (if all you mean is that his "arguments" are just about worthy of a dead horse's stench), sure, but then why should they left un-rebutted, or, worse, semi-reinforced like he might take they are by @Null's ambiguous one-liner comment? If he, or any reader of the thread, hasn't thought of something absolutely and blatantly obvious that shows once again the "quality" of his arguments (and if he had, or @Null's had, why not even mention it?!) then it better be pointed out clearly.
Alex Martelli
@Alex: I don't really care about your total rep and how it increases. I care about the effect that the "Oh, vote for Alex, he's always right!" effect has on the answers to individual questions. I also care, to a lesser extent, about how this affects the other people who answer the question who don't have as high a rep. But you're right, this belongs on meta, and I won't answer here any longer.
Omnifarious
@Omni, by all means move to meta -- if you don't care about total rep, what's "the game" you referred to earlier -- unfounded attacks on answers, and people, for its own sake? Upvotes are obviously not mutually exclusive, so somebody upvoting an answer from A that they like isn't any less likely to upvote another as well (again: if you doubt that, use the SQL-accessible data dump to prove your concerns, rather than blabbering endlessly without any data, either here or on meta). My 0 rep when I joined (>6 months after many others who already had stellar reps) clearly didn't hurt _me_, right?-)
Alex Martelli
+5  A: 

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.

delnan
+2  A: 

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.

MattH
Would the down-modder have the good grace to explain what is wrong with this answer?
MattH
@MattH There's a lot of that going on here :P
Skilldrick
+1  A: 

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.

Nathan Sanders