[Python-Dev] Examples for PEP 572

Nathaniel Smith njs at pobox.com
Wed Jul 4 14:41:38 EDT 2018


On Wed, Jul 4, 2018, 09:09 Steven D'Aprano <steve at pearwood.info> wrote:

> On Wed, Jul 04, 2018 at 12:10:11AM -0700, Nathaniel Smith wrote:
>
> > Right, Python has a *very strong* convention that each line should
> > have at most one side-effect,
>
> import math, fractions, decimal
>
> (PEP 8 be damned, sometimes, especially in the REPL, this is much
> better than three separate imports.)
>

Sure, the nice thing about "very strong convention" versus "strict rule" is
that there's a bit of lee-way at the edges. I'm pretty sure PEP 572 would
not be accepted if it's proponents were expecting it to be forbidden by PEP
8 though.


> values = [mapping.pop(key) for key in (1, 3, 6, 15)]
>
> Admittedly, that's an awfully specific case. But would you really
> re-write that as:
>
> values = [mapping.pop(1)]
> values.append(mapping.pop(3)
> values.append(mapping.pop(6)
> values.append(mapping.pop(15)
>
> just to keep the "one side-effect per line" rule? I wouldn't.
>

I don't think I've ever seen something like this in real life, but sure,
I'd say it's pushing the edge of good taste, but maybe just squeaking under
the line.

A more common edge case for my rule would be:

x = d.pop()

where one could argue that this is doing two things: (1) removing an item
from a container, and (2) storing it in a variable. But that's ok for me
argument, because my argument is about how people represent the code in
their mind so they can reason about it, and "take a value from *here* and
put it *there*" is still simple enough to feel like a single operation, and
there's no possibility of confusion about what order things happen in.
"Import these three modules" is similar.

This is also why I'm ok with

if x := fn():

But my mind stack-overflows when trying to read

if (x := fn()) is None:

The first is a complex operation, that both triggers a jump and mutates a
variable, which is two side-effects. But the two side-effects are closely
linked and cannot affect each other, so I can think of it as one "save and
jump" operation. In the second case, there's clearly 3 separate phases that
I have to simulate in my head: first the mutation of 'x', then switching
back to expression evaluation to do the comparison with None, and then the
'if'.

And of course it gets much worse in the PEPs motivating examples, where a
variable is created and used within the same expression.

This is a whole mode of reasoning about code that python has never required
before (in practice), and even if you think := is fabulous you should still
at least be able to acknowledge that this is a huge shift.


> Anyway, since "one side-effect per line" is just a convention, there's
> absolutely no reason why it cannot remain a convention. Don't do this:
>
> values = (x := expensive_func(1, 2))+(y := expensive_func(3, 4)) + x*y
>
> unless you really have to. It's just common sense.
>

I think this really underestimates the importance of this kind of
convention in how people learn and use the language, and how the language
and it's conventions influence each other.

If list.sort returned self, then people would absolutely write things like

mylist.sort().reverse()

And then we'd have lint rules and arguments about it and we'd have to waste
precious brain power deciphering this in other people's code and arguing
about it in code reviews. By not returning 'self', the language clearly
takes a position that you shouldn't do this, and cuts off all that
discussion.

The := operator is exactly the opposite: it's *only purpose* is to break
this convention, which is a pretty strong statement that now we are
supposed to embed side effects inside expressions, and be prepared to read
code that does this.


> Conventions are good. If they'e enough to stop people writing:
>
> mylist = mylist.sort() or mylist.reverse() or mylist
>
> they'll be good enough to stop people stuffing every line full of
> assignment expressions.
>
>
>
> > and that if it does have a side-effect
> > it should be at the outermost level.
>
> I don't understand what that means. Do you mean the global scope?
>

I mean something like "the top node of the statement's AST".

A normal statement: print([a])

Technically only one side-effect in this line, but it's in a weird place:
[print(a)]


> > I think the most striking evidence for this is that during the
> > discussion of PEP 572 we discovered that literally none of us –
> > including Guido – even *know* what the order-of-evaluation is inside
> > expressions.
>
> I'm not sure that "literally none of us" is quite true, after all the
> code is deterministic and well-defined and surely whoever maintains it
> probably understands it, but even if it were true, I don't see the
> connection between "we don't know the order of operations" and "Python
> has a rule no more than one-side effect per line". Seems a pretty
> tenuous ccomclusion to draw.
>

Side-effects and sequencing are intimately linked. When you have
side-effects, you have to think about their order; contrariwise, in pure
code, order of evaluation is a meaningless concept. Therefore, Python
programmers have to know the order of evaluation inside expressions exactly
to the extent that they embed side-effecting operations inside of
expressions.


>
> > In fact PEP 572 now has a whole section talking about the
> > oddities that have turned up here so far, and how to fix them. Which
> > just goes to show that even its proponents don't actually think that
> > anyone uses side-effects inside expressions, because if they did, then
> > they'd consider these changes to be compatibility-breaking changes.
>
> If you're talking about fixing the scoping issues inside classes, that's
> no longer part of the PEP (although the current version hasn't been
> updated to reflect that). The weirdness of class scopes already exist.
> This might, at worst, make it more visible, but it isn't going to make
> the problem worse.
>
> If you're talking about something else, I can't think of what it might
> be. Can you explain?
>

I'm talking about things like the PEP's discussion of what order keys and
values should be evaluated in dict displays. Previously, no one knew or
cared about this order (outside from, yes, a tiny handful of people
maintaining this bit of the compiler). Now that we're adding a language
construct whose whole purpose is to embed side-effects inside expressions,
this suddenly becomes something we all have to actually think about.


>
> > Some people make fun of Python's expression/statement dichotomy,
> > because hey don't you know that everything can be an expression,
> > functional languages are awesome hurhur,
>
> Oh, you mean functional languages like Rust, Javascript, and Ruby?
>
> (Or at least, *almost* everything is an expression.)
>
> The functional programming paradigm is a lot more than just "everything
> is an expression", and very much *non*-functional languages like
> Javascript can be designed to treat everything as an expression.
>

Sure, obviously languages can be designed that way. And there's a tradition
which argues that all languages *should* be designed that way, which I'm
hand-wavily attributing to "functional folks" – it goes back to Lisp,
ultimately, and in functional languages having a distinction between
statements and expressions *is* weird and artificial. JS, Ruby, and Rust
aren't functional languages, but in this respect they were very much
influenced by this functional tradition.

Python quite intentionally diverges. Why? Why bother distinguishing
statements and expressions at all? Python doesn't normally introduce
concepts like this for no reason. Is JS just a more elegant and
thoughtfully designed language than Python? Had Guido just never heard of
Lisp? Obviously not.

Python has a statement/expression dichotomy exactly because Python is
imperative, and the basic program structure is "do this, and then do this,
and then do this", where each "do this" is one side-effecting operation. In
this context it's super helpful to have a really clear, privileged
syntactic encoding of the things you "do" (statements) and their relative
ordering (suites).

-n
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-dev/attachments/20180704/bca1a46b/attachment-0001.html>


More information about the Python-Dev mailing list