[HN Gopher] Multiple assignment and tuple unpacking improve Pyth... ___________________________________________________________________ Multiple assignment and tuple unpacking improve Python code readability (2018) Author : bjourne Score : 142 points Date : 2022-05-08 12:34 UTC (10 hours ago) (HTM) web link (treyhunner.com) (TXT) w3m dump (treyhunner.com) | a_c wrote: | While I agree with the article, readability is rarely precisely | defined. What appeared to a veteran programmer be readable can be | confusing to novice programmer. It then begs the question aren't | veterans meant to understand a program faster. I have seen teams | spend countless hours arguing what makes code readable | 0x0000000 wrote: | I share your perspective. When I saw this line below in the | article, I thought it was neat and might use it when called | for, but I can already see the comments on code review from | some team members I've worked with who would find it less | readable than the hard coded indices alternative. | head, *middle, tail = numbers | shadedtriangle wrote: | This is probably my most used C++17 feature too: | for (auto& [key, value] : persons) | | The nested bindings are neat. Don't think you can do that in c++ | yet. | quibono wrote: | For me it's these little things that make working with newer | C++ standards so much easier. Always felt like iterating over | anything was cumbersome. No longer the case. | [deleted] | carapace wrote: | I didn't see it mentioned (forgive me if I missed it) but you can | also assign to slices: >>> n = list(range(10)) | >>> n[::3] = 'fizz' >>> n ['f', 1, 2, 'i', 4, 5, | 'z', 7, 8, 'z'] | | It's weird but occasionally useful. | BiteCode_dev wrote: | As a python veteran, I'm quite envious of the advanced | destructuring you get in some languages. | | Unpacking doesn't let you specify default values if cardinality | doesn't match, nor can you extract dictionaries by keys. | | Since operators.itemgetter and itertools.slice are limited, I end | up either adding a dependency or reinventing the wheel for every | project. | d0mine wrote: | there is a pattern matching in Python 3.10 (match/case) which | can match dictionaries. | https://peps.python.org/pep-0636/#going-to-the-cloud-mapping... | BiteCode_dev wrote: | Yes, but since it's an expression it is very verbose for a | simple assignment. | | Python really needs something like: {foo: | "default", mapping} = mapping | [deleted] | z3c0 wrote: | > Unpacking doesn't let you specify default values if | cardinality doesn't match, nor can you extract dictionaries by | keys. | | I'm a bit confused by this one, but maybe I'm misunderstanding. | Does this not accomplish what you're defining? | def parse_record(first=0, second=1, **all_the_others) -> dict: | return dict(first=first, second=second, **all_the_others) | dictionary = dict(first=99, third=2) | parse_record(**dictionary) > {"first": 99, "second": | 1, "third": 2} | bjourne wrote: | That's packing. Unpacking would be something like this: | d = {'a' : 10, 'b' : 20, 'c' : 30} {'a' : v1, 'b' : | v2, **others} = d print(v1, v2, others) # prints 10, | 20, {'c' : 30} | BiteCode_dev wrote: | Exactly. | | The working code in Python, as a function, would be: | def extract(mapping, *args, **kwargs): for a in | args: yield mapping[a] for key, | default in kwargs.items(): yield | mapping.get(key, default) | lysecret wrote: | This is what i liked most working a bit in JS. How you can | deconstruct, even in the function definition!!! Using {}. Why | did they have to use that for sets in python ahhh. | | Also using ? For attributes that might be there and being able | to chain that is sooo awesome. | | These two things make for example working with nested Json in | JS so much better than in Python. | BiteCode_dev wrote: | The ? attribute would indeed be a welcome addition, but not | to work in the same way as in JS. In JS, it ignore null | values. That wouldn't work in python. | | We would need a ? to says "ignore AttributeError, IndexError | and KeyError on that access". | | However, the use of {} for set is not a problem. | | First, remember than Python is much, much older than JS. | Insight is 20/20. Second, you could always use a different | syntax. | [deleted] | bmitc wrote: | > Unpacking doesn't let you specify default values if | cardinality doesn't match | | Functional languages that have pattern matching have wildcard | matches that allow you to explicitly handle things that don't | match as expected and also have tail matching for lists. | | > nor can you extract dictionaries by keys | | I'm not exactly sure what you mean here, but you can definitely | do that for records in F# and maps/structs in Elixir. It's done | all the time. | bobbylarrybobby wrote: | I think the parent comment is referring to how you can do | e.g., `let {x, y, z} = point` (JS, object/dictionary) or `let | Point {x, y, z} = point} (Rust, struct) but in Python your | only option is `x = point.x; y = point["y"]` etc | bmitc wrote: | I misread their first sentence. I see now that they're | lamenting what I mentioned not being in Python. I was | thinking they meant in general and not just in Python. | BiteCode_dev wrote: | Indeed. | | Now you can do: import operator | foo, bar, baz = operator.itemgetter('foo', 'bar', | 'baz')(mapping) | | But it's verbose, inelegant, and doesn't handle missing | values. | dominotw wrote: | this list is awesome. made my morning. | | anyone know why this works in python lst=[] | lst+=2, # note , in the end print(lst) #[2] | carapace wrote: | The comma operator makes tuples. lst += (2,) | raymondh wrote: | Roughly, that is true. However, the actual pattern matching | rules are a bit more complex: () # | Makes a zero-tuple. The parens are required. 2, | # Makes a one-tuple. (2,) # Makes a one-tuple. | The parens are optional. [2] # Makes a | list of size one. [(2)] # Also makes a list of | size one. [2,] # Also makes a list of size one. | f(x, y) # Call a function with two arguments. | f((x, y)) # Make a 2-tuple and call a function with one | argument. | carapace wrote: | Yeah, you're right, of course. I should have mentioned the | concept of "comma operator" is a heuristic, not something | from the grammar or semantics. | | BTW, kind of a tangent, I've always assumed (but not | checked) that every "()" literal evaluates to one singleton | zero-tuple, do you know if that's true? | HelloNurse wrote: | The tuple is sequence-like enough to be concatenated with a | list: []+(2,) differs from []+[2] only by the type of the | temporary object that contains 2. | klyrs wrote: | The truly weird thing here is that [] + (2,) | | is a TypeError: can only concatenate list | (not "tuple") to list | | but L = [] L += (2,) | | is totally cool. | | In the end, it makes sense because I can't tell you if []+() | should be a tuple or a list, and even if I did, I might have | a different answer for ()+[]; whereas L+=bla looks like the | type of L should not change. | maleldil wrote: | They're different operations. The first is concatenation; | the second is extension. Concatenation only works with | other lists, but extension works with any iterable. | sitkack wrote: | This is an excellent article! | | In functional languages this is called "destructuring | assignment". | | This article could be a great jumping off point into | namedtuple[1], the walrus operator [2] and the Pattern | Matching[3] (Python 3.10), which assist into getting back into | the symbolic domain from the structural domain, though you will | have to come up with names! :) | | Remember that namedtuple has the following additional methods | _make Class method that makes a new instance from | an existing sequence or iterable _asdict | Return a new dict which maps field names to their corresponding | values _replace Return a new instance of the | named tuple replacing specified fields with new values | _fields Tuple of strings listing the field names | _field_defaults Dictionary mapping field names to default | values. | | [1] | https://docs.python.org/3/library/collections.html#collectio... | | [2] https://peps.python.org/pep-0572/ | | [3] https://peps.python.org/pep-0636/ | agumonkey wrote: | one step at a time people will realize mainstream imperative | programming and its derivative has been a long waste of time | nerdponx wrote: | I don't think that's a valid conclusion here at all. | agumonkey wrote: | the amount of code that was - point to a | structure / object - declare a variable - | access the object / structure field and assign it to var | - repeat for every information you want to extract from the | struct / object | | is large and now replaced by something transparent | Espressosaurus wrote: | In what way is multiple assignment and tuple unpacking | _not_ imperative code? | sitkack wrote: | Assignment is not necessarily mutation. Every language, | including functional ones can assign values to names. You | have unlocked +1 reductionist pedantry. | bmitc wrote: | > In functional languages this is called "destructuring | assignment". | | I thought it's just called pattern matching. Most functional | languages don't have assignment and have bindings instead. | greggyb wrote: | "Pattern matching", in the context of functional programming | languages, most often refers to the complex combination of | destructuring bind _and_ conditional control flow based on | which binding succeeded. | | This article describing the Python feature of multiple | assignment demonstrates only the destructuring assignment | (assignment, because it is Python and these variables can be | reassigned). There is no control flow tied to this operation, | except for the exceptions that can be thrown when the LHS and | RHS have incompatible structure. | | Clojure is another example of a language with destructuring | binding that is not tied to a control flow construct. Clojure | functions and `let` bindings can use a destructuring syntax | to bind to elements in a data structure. There is no tightly- | linked control flow structure that goes along with this | binding mechanism. It is my understanding that there are | several libraries available that implement pattern matching, | but I do not use these. | | Most often, when I have seen pattern matching described to an | audience with a background in imperative languages, it is | compared to a super-powered switch statement. This gives the | impression that the important part is the control flow. | Languages such as Python and Clojure show that there is | inherent value in the destructuring assignment/bind, even out | of the context of control flow. | nerdponx wrote: | I would argue that destructuring is a special case of pattern | matching. | raymondh wrote: | IOW, the match/case tool can pattern match and destructure | at the same time. Som uninterestingly, it is possible to do | only one of those things: | | Iterator unpacking: first, *middle, last | = t | | is similar to a match/case with a sequence pattern: | match t: case first, *middle, last: | ... | | The former works for any iterable and the latter requires a | sequence. The former raises an error for a non-iterable and | the latter just skips the case for a non-sequence. | Otherwise, they are similar. | | However, arguing that one is a special case of the other | misses the entire point of structural pattern matching. We | would almost never substitute the latter for the former. | They mostly live in different worlds because we use them | and think about them much differently: # | Something that would work but they we don't do for | item in somedict.items(): match item: | case [key, value]: print(key.upper()) | print(value) print() | case _: raise RuntimeError('unexpected | kind of item') # What we actually do | for key, value in somedict.items(): | print(key.upper()) print(value) | print() | | Sorry for the small rant, but I think this pedantic point | isn't a useful line of thinking. That concepts and | implementation have some overlap but should occupy a | different concept maps in our brains -- very much like the | idea that dicts can do set-like things but that we're | better off thinking about dicts as doing lookups and sets | as doing uniquification, membership-testing, and set-to-set | operations. | bobbyi wrote: | I definitely expected "multiple assignment" to refer to this | syntax (which Python also has) x = y = None | anyaya wrote: | I had no idea about the '*'!! | z3c0 wrote: | Great article. I use unpacking regularly for a lot of the reasons | given, as it helps ensure data is in its expected shape. However, | I never knew of "star assignment" eg first, | second, *others = some_tuple | | I've opted for less-readable positional indexing several times | for not realizing this. Looks like I have some updates to do. | JadeNB wrote: | > However, I never knew of "star assignment" eg | first, second, *others = some_tuple | | For what it's worth, I think that's often called "splat". | delaaxe wrote: | You can also do: first, second, *_ = | some_tuple | | If you don't care about the others | jwarden wrote: | This reminds me of an idea I wrote about years ago called Pass- | Through Lists, where every assignment is implicitly a | destructuring assignment: | https://jonathanwarden.com/2014/06/19/pass-through-lists/ | [deleted] | divs1210 wrote: | Destructuring is one of the things i love about Clojure! | | I'm happy that js and python have also adopted it. | yawaramin wrote: | I'm fairly sure Python had destructuring syntax before Clojure | existed. I remember using it in early 2000s. | Ambolia wrote: | A bit sad that this stopped working in lambdas in Python 3, it | was nice to have in Python 2: (lambda (x, y): | x+y)((2, 3),) | raymondh wrote: | I also miss this capability. It was super helpful for working | with 2-D or 3-D point vectors: lambda (x1, | y1), (x2, y2): return abs(x2 - x1) + abs(y2 - y1) | anfractuosity wrote: | That's pretty cool, shame it doesn't work in 3 too. | | Not as nice, but: (lambda x, y: x+y)(*(2, 3)) | | Seems to work | klyrs wrote: | Now do lambda i,(x,y): x[i]+y[j] | | I never really understood the rationale here. Pep8 hates | lambdas, which is also baffling. It seems as though tptb | don't like lambdas, but there wasn't enough political will to | remove them entirely, so they kneecapped them instead. | Because... more lines = more readable?? | | But we've got a walrus operator now, because... less lines = | more readable? | sitkack wrote: | You golf a lambda and then deride walrus in the same | comment? Please explain yourself. | | And if you really want to make the perfect lambda, you can | do that with code generation and frame walking/patching. | | http://farmdev.com/src/secrets/framehack/index.html | klyrs wrote: | That wasn't golf. Are you going to tell me that this is | more legible? lambda i, t: t[0][i] + | t[1][i] | | edit: and... that frame hack thing is fun, but what does | it have to do with anything? | | Also, note that I don't "deride" the walrus operator. But | it does smack of inconsistency. Are we aiming for clean | notation or not? | sitkack wrote: | You want tuple params back into an inlined function | declaration? You can do that with a framehack. One of the | examples from the link is Ruby style string | interpolation, f-strings in Python, before f-string | support. | | You can fight Python on lambda support, or you can build | the language you want. It gives you the tools. | | I am not arguing about lambda legibility, but tilting at | windmills rather than just walking around them. They | can't move. | abecedarius wrote: | Do you really use framehacks in production for the sake | of syntactic sugar like this? It's fair that Python _can_ | do this sort of thing[0], but it seems like begging for | trouble to make it load-bearing without a very strong | reason. | | [0] That level of access was essential when I was working | on https://codewords.recurse.com/issues/seven/dragon- | taming-wit... | sitkack wrote: | If the local sugar leads to a lower incidence of | diabetes, then yes. | | I have never had a framehack spontaneously break. Self | proclaimed Pythonistas of course make a sour face, like | their God was offended, but that isn't a logical argument | against. | | I really enjoy your compiler art, please keep it up. Are | you doing anything with Wasm these days? | wnoise wrote: | The example lambda isn't "golfing". It's just plain more | readable than naming the function. | | Introducing side-effects in the middle of expressions | with the walrus operator is weird semantics, though | admittedly sometimes very convenient. | xigoi wrote: | I like the walrus operator for working with the regex | module. if match := re.search(r"Score = | (\d+)", data): print(match[1]) else: | print("Not found") | tln wrote: | FYI here's the thread where tuple parameters were removed. | | https://mail.python.org/archives/list/python- | dev@python.org/... | klyrs wrote: | > So the short answer is: yes, we are attached to it, we | will miss it. No guarantees about throwing fits, though | <wink>. | | Observe: me, throwing a fit. | carapace wrote: | I cursed and swore for twenty full minutes when they | removed tuple unpacking from parameters. God that pissed | me off. Sure, I was one of only a handful of people who | ever used it, and it had it's warts, but it was so | elegant. | klyrs wrote: | >>> Hey, we don't like this feature anymore, is anybody | using it?" | | >> Yes, we're using it" | | > Okay, it's gone" | | [rage] | | The one thing I agree with in the thread is the confusion | of foo = lambda (x,y): x+y foo(1,2) | #boom | | I think a better approach would be to raise warnings when | sole parameters are parenthesized, unless written as | foo = lambda (x,y),: x+y | | which is kinda gross, but cogent with the weirdness of | singletons: a, = [1] b = (1,) | c = 1, | | Alas, I've never had Guido's ear, so I shout into the | void. | raymondh wrote: | People who respect the law and love sausage should never | watch either of them being made ;-) | | The referenced thread isn't Python's finest hour. A: | "Hey, this feature is slightly inconvenient to implement | in AST, let's kill it." B: "Many users haven't heard of | this, so let's kill it." C: "I find it useful and there's | not real advantage in killing it". A&B: "Okay, it's | gone". | | Really, you're better-off just enjoying a language that | is mostly nice and mostly well-designed. Don't read the | development threads. It will spoil the illusion. | macintux wrote: | As an Erlang devotee, when I switched jobs and started writing | Python I was happy to find that destructuring worked for | function arguments, only to be disappointed when I realized | Python 2 was about dead and Python 3 had dropped it because "no | one uses it". | [deleted] | moelf wrote: | that's all cool and fine, but can you unpack named tuple or | struct (data class)? In Julia one of my favorite quality of life | improvement in the 1.7: | https://github.com/JuliaLang/julia/pull/39285 | | Example: julia> X = (this = 3, that = 5) | (this = 3, that = 5) julia> this ERROR: | UndefVarError: this not defined julia> (;that, this) | = X; julia> this 3 julia> that | 5 | | this works with structs as well (in fact anything with | getproperty() defined, so like dataframe column too) | kzrdude wrote: | namedtuple yes, it's a tuple so can be unpacked (by position) | and dataclasses can not be unpacked. But with the new pattern | matching, you can "unpack" dataclass objects that way! | | https://peps.python.org/pep-0636/ | moelf wrote: | but I'm showing unpacking by name, not by position. | kzrdude wrote: | I answered the question you put a question mark behind :) | girzel wrote: | I've enjoyed using types.SimpleNamespace: >>> | from types import SimpleNamespace >>> d = {"one":1, | "two":2} >>> ns = SimpleNamespace(**d) >>> | ns.two 2 | | I consider it a bonus that the dict keys are grouped as | attributes on an object :) | raymondh wrote: | As I teacher, types.SimpleNamespace is wonderful for helping | people bridge between their understanding of dicts (a | fundamental) to classes/instances (a little less | fundamental). | | However, I almost never use SimpleNamespace in real code. | Dicts offer extra capabilities: get, pop, popitem, clear, | copy, keys, values, items). Classes/instances offer extra | capabilities: unique and shared keys in distinct namespaces, | instances knowing their own type, and an elegant | transformation of method calls ``a.m(b, c)`` --> | ``type(a).m(a, b, c)``. Dataclasses and named tuples have | their advantages as well. In practice, it is almost never | that case that SimpleNamespace beats one of the alternatives. | jwilk wrote: | If you don't mind questionable hacks, you can implement it in | Python by abusing the import system: | | https://stackoverflow.com/a/69860087 | est wrote: | looks can be done with some weird locals().__setitem__ hack . | jwilk wrote: | JavaScript has it too; it's called "object destructuring": | | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... | rsfern wrote: | That's cool that this is in Julia Base! Do you know how it | compares to Unpack.jl? I love this feature because you can do | partial unpacking by name | | https://github.com/mauro3/Unpack.jl | moelf wrote: | Unpack.jl happened before this got added to Julia Base. | | You can always partially unpack, just only put what you need | on the LHS | woodruffw wrote: | I've long wanted some kind of "null-filling" destructuring | operator in Python, to avoid cases like this: # | oops, ValueError on `item = "noequals"` key, value = | item.split("=", 1) | | That alone makes destructuring somewhat limited, unless I'm | either willing to validate my lengths ahead of time or do | exception-driven control flow. Something like `?=` perhaps, to | complement the walrus operator. | tech2 wrote: | Is empty string acceptable in place of None? If so, consider | partition: key, sep, value = | item.partition("=") | | If you need to know whether there was a split/match that | occurred you could test sep's truthiness. | klyrs wrote: | Does this work for you? key, *value = | item.split("=", 1) if value: value = value[0] | else: value = None | woodruffw wrote: | Yep, that's a pattern I've applied before. It's not the | worst, but I don't think it has the clarity that this would | have: key, value ?= item.split("=", 1) | | Modulo bike shedding over the actual syntax, of course. | | (Edit: the semantics of this could be even more generic, like | Ruby's `[...] rescue value` syntax for turning any raised | exception into a default value instead.) | xtreak29 wrote: | There was a similar PEP https://peps.python.org/pep-0505/ | srfwx wrote: | value = value[0] if value else None | [deleted] | ttyprintk wrote: | I suppose I'd like the smoke-filled rooms deciding readability to | mandate square brackets on the left-hand side, or permit ,= | without a space when a tool like black has an opinion. | raymondh wrote: | Wouldn't it have been better for the language itself to have | made a decision rather than another group of people who are in | the business of having strong, enforced decisions about what is | allowed? | | The language specifies that simple statements can be separated | by semicolons. Black eliminates this part of the language, | essentially overriding Guido's decision. | | The language specifies that four different quoting characters | are available. Black eliminates this part of the language, | mandating double quotes in code and single quotes in a repr | output, essentially overriding Guido's decision. | | The language specifies that in-line comments can start anywhere | on a line. Black eliminates the writer's option to line-up | their comments vertically, essentially overriding Guido's | language decision. | | The language allows and PEP 8 encourages using whitespace to | aid readers in reading mathematical expressions just like sympy | and LaTex do: ``3 _x*2 - 2_ x +5``. Black eliminates this | possibility and mandates equal spacing regardless of operator | precedence: ``3 * x * 2 - 2 * x + 5``. Once again, this | overrides Guido's judgment. | | Wouldn't be better for Guido to decide what the language allows | rather than have a downstream tool narrow the range of what is | possible? | digisign wrote: | Yes, and I'm looking forward to blue, and have used nero as | well before I knew blue existed. Like the idea of black, and | implementation, but its "taste" unfortunately leaves a bit to | be desired. | | Pushing extra spaces into slices is another one I don't care | for. ___________________________________________________________________ (page generated 2022-05-08 23:00 UTC)