6 ms·
You can whitewash this away by discussing order of evaluation, but it still is a great example of how Python continually mixes up concepts of expressions and st
by bmitc 3y ago
You can whitewash this away by discussing order of evaluation, but it still is a great example of how Python continually mixes up concepts of expressions and statements and immutable and mutable data. In real life, if I ask someone to go get me four empty buckets, I expect them to come back with four distinct empty buckets. The intuitive interpretation of [[]]*4 is asking just that. Instead Python creates a bucket and then gives you a piece of paper that lists its address four times. Why does one want that, by default?
In more sane languages, the expression [] means give me an empty list value, nothing more and nothing less. So then whatever the syntax for [[]]*4 is in such sane languages, it gives you four empty list values. If you want something more imperative, you can get it, but it most cases, you don't want or even need such things.
Python makes no distinction, at a level able to be visualized by the user, between immutable and mutable data. There are a lot of bizarre situations in Python that will bite you like this. And in most cases, it's strange that such behaviors were chosen as the deault.
- Joker_vD 3y agoI really wonder what "more sane" languages you mean. Because a single [] does give you an empty list value in Python: one list, and not four different empty lists. Why would it give your four? That'd need to somehow bring in the surrounding context and in "sane" languages, (closed) expressions generally don't change the meaning because the context around them is changing. Actually, if you really want to have re-evaluting (sub)expressions, then Python does give you an option: comprehensions.
- bmitc 3y agoF#: List.init 4 (fun _ -> []) [for _ in 1..4 -> []] Or any sane language that uses immutable values by default like Elixir, Erlang, Clojure, Racket, Scheme, OCaml, etc. > Actually, if you really want to have re-evaluting (sub)expressions, then Python does give you an option: comprehensions. That's the complaint, that [[]]*4 is not shorthand for list comprehensions and that it's something entirely different. This is common in Python where two things you'd expect to be the same are indeed different in subtle ways. List comprehensions in Python are closer to sane, but they still have unexpected behavior because of the way scoping works in Python.
- arijun 3y agoWhy are you comparing functions/closures with values? You can do the same in python: [list() for _ in range(4)]
- bmitc 3y ago> Why are you comparing functions/closures with values? I'm not. The fun _ -> [] is needed because the argument is the element's index. That's just what List.init happens to expect, a function that takes an index and initializes the element, but the index is not needed here.
- arijun 3y agoAre you saying f# special-cases something that looks like a closure, but isn’t in this one context? Because that, to me, seems like a much more grievous issue than the one at hand.
- bmitc 3y agoNo. F# lambdas are closures. I was just saying that I'm not using the concept of closures to explain here. It's just a side effect of using the List.init function as a demonstration.
- arijun 3y ago(I can’t respond to your other comment) Ah I see, you’re saying that the construction of a list in F# mandates a lambda, thereby reducing one possible point of confusion. I could agree with that. But then it comes at a cost of comfort: [0]*4 has less boilerplate than the F# version. So it comes down to trade-offs again.
- dragonwriter 3y ago> That's the complaint, that [[]]4 is not shorthand for list comprehensions There are lots of reasons you would want items in a list repeated rather than having, essentially, the result of separate executions of the same constructor -- [] is just shorthand for list() -- so its good that [x()]4 is not shorthand for [x() for _ in range(4)]. Especially since that would mean it would have to be special-case sytax for list construction, and not a list operator that you could use on existing lists. The mildly annyoing edge case you need to learn about isn’t worth wiping out the whole broadly-useful language feature, or creating a great special-case inconsistency which would be more cognitive load to deal with, to eliminate.
- arijun 3y agoLanguages with immutable data structures are relatively rare. With its high level focus python could have been one of them, but went for different trade offs. > In more sane languages, the expression [] means give me an empty list value, nothing more and nothing less What is an “empty list value”?
- dragonwriter 3y agoPython has immutable data structures, lists just aren't one of them. (Tuples are. So are, unlike in some languages, strings.)
- thomasahle 3y ago> I expect them to come back with four distinct empty buckets. The intuitive interpretation of [[]]*4 is asking just that. I think the small example is misguiding the intuition here. If I write `my_list = f(expensive operation); long_list = my_list * 4`, I don't expect python to recreate the whole thing four times. I justly assume `f` will be evaluated once, and then copied.
- bmitc 3y agoThat's a different statement and expected, but in a language like Python, it isn't always clear whether my_list is a value or a reference. And because of that, it leads to unexpected behavior when an intuitively expected value is actually a reference.
- maxcoder4 3y agoI think you're confused about the difference between [x]*4 and [x for _ in range (4)]. It's not, at any point, a difference between something being a value or reference. The first code is equivalent to: list_multiply([x], 4) (the function is actually called list.__mul__, but that's an unimportant detail). The latter is a syntax sugar for: out = [] for _ in range(4): out.append(x) (actually a bit more complex, but close enough). When you understand it, the behavior is obvious in both cases.
- zarzavat 3y agoLists in Python are always references. There’s a separate type called tuple specifically designed to represent lists-as-values. If you do [()]*4 then you get exactly what you want.
- dragonwriter 3y ago> There’s a separate type called tuple specifically designed to represent lists-as-values. tuples are also references, but to an immutable object. > If you do [()]*4 then you get exactly what you want. I think what is actually wanted here is four different mutable lists rather than the same mutable list four times, something like what you would get from: [[] for _ in range(4)]
- arijun 3y agoLanguages with immutable data structures are relatively rare. With its high level focus python could have been one of them, but went for different trade offs. > In more sane languages, the expression [] means give me an empty list value, nothing more and nothing less What is a “list value”? It’s either a pointer to some other space where the lists values are stored, or it’s the values stored themselves. If you say it’s the second (I can’t think of any language that does), then `a = […]; b = a` is an expensive copy by value assignment. Is that worth that trade off to get less surprising results in the articles case? I’m not saying the answer is “obviously no”, but it definitely isn’t “obviously yes”.
- karelpeeters 3y agoMatlab is one language that chooses the second. Every assignment a=b where b is a matrix creates a distinct copy of the matrix. The interpreter is slightly more clever and instead implements this as a copy-on-writr mechanism, but that's just an implementation detail .
- Someone 3y ago> If you say it’s the second (I can’t think of any language that does), then `a = […]; b = a` is an expensive copy by value assignment In Swift collections are value types, so b = a conceptually is a copy. It’s not expensive, though, because Swift does copy-on-write. You pay for that by a combination of having more expensive collection updates (they, at least conceptually, have to check whether to make that copy) and a more complex compiler (it tries to avoid having to actually do those checks at runtime) var a: [Int]=[] var b = [a,a,a,a] b[0].append(1) print(b) will produce [[1],[],[],[]]
- mypalmike 3y agoJust teaching myself Swift this week. Thanks, this is helpful!
- VS1999 3y agoAgreed. Python is aimed at developers, not language enthusiasts, and you should be able to intuit what something is doing without having to memorize dozens of obscure rules and edge cases. The less surprises a language has, the better the design.
- shiandow 3y agoIn that case what would you expect the following to return? a = [] b = [a]*4 a.append(1) print(b) I mean it is possible to change Python and make [...]*n a shorthand for "repeat this statement n times" but that would be odd on its own. Lists pass by reference, tuples pass by value, if you want to understand Python you're just going to have to get used to that distinction. There are other options, but most are worse.
- bmitc 3y ago> , if you want to understand Python you're just going to have to get used to that distinction My argument is that that distinction isn't needed, by default, in a high-level language, and that plenty of people get it wrong, understandably. It is simply complexity that is not needed. These simple examples can be toyed with, but sooner or later one is going to want to solve real problems, and unintuitive, default behavior doesn't help with that. Python's mode of operation tends to favor implicit over explicit. And the syntax [...]*n is implicit.
- Skeime 3y agoThe distinction is needed, I think, unless you want to go the purely-functional route. I love that route, but just as many people will complain about purely functional languages being unintuitive. If you want mutable values in your language, you need to expect values to mutate. So what should list multiplication do? - There is the current implementation, where a change to the first entry of [f(x)] * 4 will be reflected in all entries of the list (because they're all references to the same value). - It could only work on list literals. So [f(x)] * 4 would call f four times and collect the values in a list. Then, a = [f(x)]; a * 4 would not work, which would be surprising. - It could work differently on list literals and lists. So [f(x)] * 4 would call f four times collecting the results, but a = [f(x)]; a * 4 would just return a list of four references to the one result of f(x). - It could copy the value in the list. How would you do that if the list contained values like file handlers or network connections that can't be copied? Within the context of Python (an imperative language with mutable values), the current implementation is the most sensible one. If you work in such a language, you must always exercise some care when working with mutable values. Learn your tool! Python could have been designed with different decisions. But who knows if we would even be talking about it today in that case?
- magnio 3y ago> In more sane languages, the expression [] means give me an empty list value, nothing more and nothing less. So then whatever the syntax for [[]]*4 is in such sane languages, it gives you four empty list values. I don't buy this. Either the syntax to give you four empty list values is some kind of comprehension, which Python has and does exactly what you like, or it must have lazy evaluation somehow. Otherwise, how can the language know you don't want to copy the existing reference but create a new empty list? If [[]]*4 were to create new list on the fly, then `[print('hello')]*4` would have to print hello 4 times, i.e., the print statement is lazily executed, which does not fit into Python at all.
- bmitc 3y agoMy question is: what's a real-world example where this [...]*n syntax buys you something that can't be done simply otherwise and that is worth the complexity tradeoff of people often using it subtly wrong?
- maxcoder4 3y agoIt's syntax sugar. I use it all the time, but of course it could always be respected by, i don't know, a for loop. It also makes writing duck typed code easier, as X3 will work for strings, bytes and lists. Overall it's a very useful feature and I don't think it's that surprising. It's pretty obvious [x]4 will reuse the same reference. Just like manually adding "x" four times will.
- mapreduce 3y ago> that is worth the complexity tradeoff of people often using it subtly wrong? I don't think that makes the complexity tradeoff worth it. This is the kind of tradeoff that makes the language implementation, reference and semantics more and more complex. Unfortunately Python has a lot of such complexities already. We don't need more of it. Such complexity hurts the creation of completing implementations. IMHO programming language specifications and implementations should be simple and consistent so that competing implementations can be developed easily. > people often using it subtly wrong? I think that... you know... before writing serious software, these people need to roll up their sleeves and just learn the damn language. Just learn what references are and how they work in Python. It's not that hard. It is most definitely easier than adding a sphagetti monster to the compiler make it bend to the will of programmers who can't be bothered to read the tutorial. I mean this gotcha about references is taught in the 4th chapter of the tutorial. Can't developers even bother to RTFM these days before developing serious software with a programming language?
- kerkeslager 3y ago> In real life, if I ask someone to go get me four empty buckets, I expect them to come back with four distinct empty buckets. That's not what you're asking for, though. You're asking for one bucket, and then you're putting it in another bucket, and then you're duplicating the contents of the second bucket four times. This isn't whitewashing, this is just what those expressions mean. It's not like it's hard to get four buckets in Python, BTW: [[] for _ in range(4)]
- hughesjj 3y ago> immutable and mutable data I don't get how this is an example of python mixing up mutable/immutable data, and I'm unaware of a situation where python does. Conceptually I'm not even sure what you mean by a language 'mixing up mutable and immutable data' unless it's literally just a buggy/non hardened implementation. A = (1, ); A[0] = 2 # this raises an exception