[Python-ideas] How assignment should work with generators?

C Anthony Risinger c at anthonyrisinger.com
Fri Dec 1 16:04:27 EST 2017

On Nov 29, 2017 10:09 PM, "Steven D'Aprano" <steve at pearwood.info> wrote:

> On Thu, Nov 30, 2017 at 11:21:48AM +1300, Greg Ewing wrote:
> > It seems that many people think about unpacking rather
> > differently from the way I do. I think the difference
> > is procedural vs. declarative.
> >
> > To my way of thinking, something like
> >
> >    a, b, c = x
> >
> > is a pattern-matching operation. It's declaring that
> > x is a sequence of three things, and giving names to
> > those things. It's not saying to *do* anything to x.
> I hadn't thought of that interpretation before, but now that Greg
> mentions it, its so obvious-in-hindsight that I completely agree with
> it. I think that we should promote this as the "one obvious"
> interpretation.
> Obviously as a practical matter, there are some x (namely iterators)
> where you cannot extract items without modifying x, but in all other
> cases I think that the pattern-matching interpretation is superiour.

This conversation about suitcases, matching, and language assumptions is
interesting. I've realized two concrete things about how I understand
unpacking, and perhaps, further explain the dissonance we have here:

* Unpacking is destructuring not pattern matching.
* Tuple syntax is commas, paren, one, or both.

For the former, destructuring, this reply conveys my thoughts verbatim:

"There are two different concerns in what people refer to as "pattern
matching": binding and flow-control. Destructuring only addresses binding.
Pattern matching emphasizes flow control, and some binding features
typically come along for free with whatever syntax it uses. (But you could
in principle have flow control without binding.)"

The only part of unpacking that is 'pattern matching' is the fact that it
blows up spectacularly when the LHS doesn't perfectly match the length of
RHS, reversing flow via exception:

>>> 0,b = 0,1
  File "<stdin>", line 1
SyntaxError: can't assign to literal

If Python really supported pattern matching (which I would 100% love! yes
please), and unpacking was pattern matching, the above would succeed
because zero matches zero. Pattern matching is used extensively in
Erlang/Elixir for selecting between various kinds of clauses (function,
case, etc), but you also see *significant* use of the `[a, b | _] = RHS`
construct to ignore "the remainder" because 99% of the time what you really
want is to [sometimes!] match a few things, bind a few others, and ignore
what you don't understand or need. This is why destructuring Elixir maps or
JS objects never expect (or even support AFAIK) exact-matching the entire
object... it would render this incredible feature next to useless!
*Destructuring is opportunistic if matching succeeds*.

For the latter, tuples-are-commas-unless-they-are-parens :-), I suspect I'm
very much in the minority here. While Python is one of my favorite
languages, it's only 1 of 10, and I didn't learn it until I was already 4
languages deep. It's easy to forget how odd tuples are because they are so
baked in, but I've had the "well, ehm, comma is the tuple constructor...
usually" or "well, ehm, you are actually returning 1 tuple... not 2 things"
conversation with *dozens* of seasoned developers. Even people
professionally writing Python for months or more. Other languages use more
explicit, visually-bounded object constructors. This makes a meaningful
difference in how a human's intuition interprets the meaning of a new
syntax. *Objects start and end but commas have no inherent boundaries*.

These two things combined lead to unpacking problems because I look at all
assignments through the lens of destructuring (first observation)
and unpacking almost never uses parentheses (second observation). To
illustrate this better, the following is how my mind initially parses
different syntax contrasted with what's actually happening (and thus the
correction I've internalized over a decade):

>>> a, b = 5, 6
CONCEPT ... LHS[0] `a` bound to RHS[0] `5`, LHS[1] `b` bound to RHS[1] `6`.
REALITY ... LHS[:] single tuple destructured to RHS[:] single tuple.

>>> a, b = 5, 6, 7
CONCEPT ... LHS[0] `a` bound to RHS[0] `5`, LHS[1] `b` bound to RHS[1]
`6`, RHS[2] `6` is unbound expression.
REALITY ... LHS[:] single tuple destructured to RHS[:] single tuple, bad
match, RAISE ValueError!

>>> (a, b) = 5, 6
>>> [a, b] = 5, 6
CONCEPT ... LHS[0] `(a, b)` bound to RHS[0] `5`, bad match, RAISE TypeError!
REALITY ... LHS[:] single tuple destructured to RHS[:] single tuple.

>>> a, b = it
>>> [a], b = it
CONCEPT ... LHS[0] `a` bound with RHS[0] `it`, LHS[1] is bad match, RAISE
REALITY ... `a` bound to `it[0]` and `b` bound to `it[1]` (`it[N]` for
illustration only!)

The tuple thing in particular takes non-zero time to internalize. I
consider it one of Python's warts, attributed to times explained and
comparisons with similar languages. Commas at the top-level, with no other
construction-related syntax, look like expression groups or multiple
returns. You have to already know Python's implicit tuple quirks to
rationalize what it's really doing. This helps explain why I suggested the
`LHS0, LHS1 = *RHS` syntax, because it would read "expand RHS[0] into



C Anthony
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20171201/822d5dbe/attachment-0001.html>

More information about the Python-ideas mailing list