How PEP-572 would change the standard library
github.com
github.com
This seems to be replacing
foo = check_foo()
if foo:
do_foo()
with if (foo := check_foo()):
do_foo()
which goes against that idea. foo = check_foo()
while foo:
do_foo()
foo = check_foo()
With while foo := check_foo():
do_foo()
which is a case that hasn't had an elegant solution in the past. Another, if example, where you want to call a function based on the result of a check, but don't want to make that check more than once: foo = check_foo()
if foo:
do_foo(foo)
else:
other_foo = check_other_foo()
if other_foo:
do_other_foo(other_foo)
else:
# Continue
with if foo := check_foo():
do_foo(foo)
elif other_foo := check_other_foo()
do_other_foo(other_foo)
elif .....:
Yes, all of this has been possible and there are other ways to do it, but this expresses the problem very concisely and has less nesting at the cost of a slightly more complicated syntax. I find these rewritten examples easier to parse than the originals.Pulling things out of comprehensions is pretty neat too.
Other than a few cases (e.g. these) I don't see this suited or intended for extremely wide usage, but no worse than any other complex idiom that people can overuse.
while 1:
foo = check_foo()
if not foo: break
do_foo(foo)
Its only downside was taking 2 extra lines, not actual understandability. while 1:
now = time()
if now > prev_time + UPDATE_TIME: report_progress()
foo = check_foo()
if not foo: break
if foo < 0: print_error(); break
do_foo(foo)
prev_time = now
Now, was it really dictating a precondition? Or was it actually dictating a postcondition? Or both or neither? Would you really want these responses to dictate the locations of the condition in the syntax? Because it seems to me that forcing yourself to put these conditions in the loop condition just makes the code extremely ugly and difficult to evolve (requiring you to duplicate code or have very convoluted expressions with assignments). foo = check_foo()
if foo:
do_foo()
Why would you ever write that?Just
if check_foo():
do_foo() foo = check_foo()
if foo:
do_foo(foo)I think this comment falls along this type of bike-shedding.
"Why would you need a new construct for something that is already solved?"
To me, adding the variable assigned before the if-statement, inside, or after the if-statement would have made it clear why one might write the original code that way, and why a new type of assignment could be helpful.
Looking at the downvotes, that is apparently not a common confusion.
Then remove 'for' as it iterates and checks for a condition at the same time
Remove list comprehensions, as they do multiple things in one single statement
Remove map/filter/zip as they iterate and apply a function
Remove the '+=' operator (though x = x + 1 still do more than one thing at the same time)
re.match should return a match object no matter what, and .group() should return strings, empty string if non were matched.
We don't even need anything remotely :=
There might be ways the regular expression library could be designed better, but making re.match return a match when there's no match is not an improvement.
The problem here is that it's bad design design if a function returns multiple different types of result. Generally there should only be one type of result to make handling reliable. It if's neccessary, you can still test for an empty result, making it the optional case that it is.
The main problem here is that you can't chain re.match().group() because None has no .group()
Why do you want to match empty string with a group?
The reason why someone might want to be able to distinguish between "no match" and "the match is empty" is writing code that takes any arbitrary regular expression and uses it as a filter. Then someone else could pass in an expression that matches empty and everything works together nicely.
if rawdata.count("\n", i, j) as nlines:
self.lineno = self.lineno + nlines
...You can still use the old way if you think this new one is ugly, but to be honest I think it's going to be an improvement.
with
if my_data := my_function():
do_stuff(my_data)
the left-hand side of the assignment is completely irrelevant to the if statement. Here you have to go "all the way to the end" just to see what that clause of the if-statement is.With the as-syntax, the if-statement and the clause are placed together.
However, I do find the if-as sentence kinda weird, when trying to understand it using normal english.
I guess is subjective? To me, that's the first thing I want to know about an assignment -- what is being assigned to? Especially if you try to keep your programs reasonably functional, then it's in fact the thing that matters, since the right-hand side would be side-effect-free.
Another way I see it is that an assignment expression is like solving a problem: first you'd ask "what did they solve for?", then you ask "how did they solve for it?" -- not the other way around.
> if rawdata.count("\n", i, j) as nlines:
I have to read the beginning of the line, then jump to find the as (which is not very obvious in the expression) then find the var name.
> if nlines := rawdata.count("\n", i, j):
Here I do a continual motion from the beginning of the line up to the start of the condition
> the left-hand side of the assignment is completely irrelevant to the if statement
It is relevant to the subsequent block of code, I need to know what 'nlines' mean.
if attached rawdata.count("\n", i, j) as nlines then ....
[1] https://www.eiffel.org/doc/eiffel/Void-safety%3A%20Backgroun...
Why not "as" syntax: https://mail.python.org/pipermail/python-dev/2018-July/15433...
(I'm just pointing to the given explanations for the interested. I'm not necessarily agreeing with them.)
root@8d88e1e678b0:/cpython# astpath -A 1 "//Assign[(targets/Name/@id = following-sibling::*[1][name(.) = 'If']/test/Name/@id)]" > pep-572.txt
root@8d88e1e678b0:/cpython# grep "Lib" pep-572.txt | head -20
./Lib/argparse.py:284 > help = self._root_section.format_help()
./Lib/argparse.py:285 if help:
./Lib/argparse.py:2275 > a = [action for action in positionals
./Lib/argparse.py:2276 if action.nargs in [PARSER, REMAINDER]]
./Lib/cmd.py:300 > doc=getattr(self, 'do_' + arg).__doc__
./Lib/cmd.py:301 if doc:
./Lib/cmd.py:356 > nonstrings = [i for i in range(len(list))
./Lib/cmd.py:357 if not isinstance(list[i], str)]
./Lib/compileall.py:123 > mo = rx.search(fullname)
./Lib/compileall.py:124 if mo:
./Lib/dis.py:386 > show_lineno = linestarts is not None
./Lib/dis.py:387 if show_lineno:
./Lib/dis.py:403 > new_source_line = (show_lineno and
./Lib/dis.py:404 instr.starts_line is not None and
./Lib/enum.py:86 > already = set(value) & set(self._member_names)
./Lib/enum.py:87 if already:
./Lib/enum.py:153 > invalid_names = set(enum_members) & {'mro', }
./Lib/enum.py:154 if invalid_names:
./Lib/enum.py:848 > negative = value < 0
./Lib/enum.py:849 # issue29167: wrap accesses to _value2member_map_ in a list to avoid race
root@8d88e1e678b0:/cpython# grep -E "\.py\:[^w]+>" pep-572.txt | wc -l
412
root@8d88e1e678b0:/cpython# grep -E "/Lib/.+\.py\:[^w]+>" pep-572.txt | wc -l
312 if a = b():
foo(a)
I know it doesn't work, but why couldn't it? if a == b():
foo(a) if (10 == x)
This way accidentally missing a "=" triggers a compiler error instead of quietly doing the wrong thing.Am I wrong to say that a bunch of code that works is changed for no other reason than to exercise the new syntax?
"The point of this PR is just to open discuss on coding style: discuss when assignment expressions are appropriate or not."
There is no new code and the author's intention seems to be more than a "let's see how it would look like" patch. (Discussing where to use new syntax on existing code is a waste of time unless the intent is to apply the changes)
https://github.com/python/cpython/pull/8122/files#diff-0ad86...
[1] https://docs.quantifiedcode.com/python-anti-patterns/readabi...
A more typical way to read from a file-like object is argumentless .read() to get all content, or iterating over it to get it line by line, but if you're passing an argument to .read you're doing something fairly low-level anyway.
EAFP is already violated in a few cases where it makes more sense. This doesn't add new violations, it adds a cleaner way to work with them.
I was reading the discussion above, where they argue whether a statement like
if (foo := check_foo()):
do_foo()
is readable. The entire time I’m thinking Willis. Tango. Foxtrot! This is the kind of stuff us C / AWK programmers do all of the time precisely because it’s concise, elegant and intuitive (not to mention well understood), and these guys are struggling since it turned into a huge discussion. Made me wonder if we’re in a programming kindergarten here or what?Really, all this mess is the legacy of poor choice of = and == as operators back in C (or rather, B). Pascal really had it right with the assignment being very distinct from comparison, such that it's pretty much impossible to confuse the two. The sole reason for that design decision in C, so far as I know, was that assignments occur slightly more often than comparisons, and so they wanted it to be a single character. But I seriously doubt that it was worth all the bugs.
No it isn’t; we’re not beginners.
I learned Pascal before I learned C. Hated the retarded := as superflous.
if (foo = func()) is not None:
...
I find this more readable and uniform than using := construct.It would also cause ambiguity with keyword arguments. You'd have to write foo((x = 3)) because foo(x = 3) already has a meaning. I think foo(x := 3) is clearer.
Maybe it would have been best to use := for all assignment. But it's too late for that.