[Python-Dev] PEP 572: intended scope of assignment expression
Victor Stinner
vstinner at redhat.com
Thu Jul 5 11:42:13 EDT 2018
After "Assignment expression and coding style: the while True case",
here is the part 2: analysis of the "if (var := expr): ..." case.
2018-07-05 14:20 GMT+02:00 Victor Stinner <vstinner at redhat.com>:
> *intended* scope.
I generated the giant pull request #8116 to show where I consider that
"if (var := expr): ..." would be appropriate in the stdlib:
https://github.com/python/cpython/pull/8116/files
In short, replace:
var = expr
if var:
...
with:
if (var := expr):
...
I used a script to replace "var = expr; if var: ..." with "if (var :=
expr): ...". I restricted my change to the simplest test "if var:",
other conditions like "if var > 0:" are left unchaned to keep this
change reviewable (short enough). The change is already big enough (62
files modified) to have enough examples! Then I validated each change
manually:
(*) I reverted all changes when 'var' is still used after the if.
(*) I also reverted some changes like "var = regex.match(); if var:
return var.group(1)", since it's already handled by my PR 8097:
https://github.com/python/cpython/pull/8097/files
(*) Sometimes, 'var' is only used in the condition and so has been
removed in this change. Example:
ans = self._compare_check_nans(other, context)
if ans:
return False
return self._cmp(other) < 0
replaced with:
if self._compare_check_nans(other, context):
return False
return self._cmp(other) < 0
(Maybe such changes should be addressed in a different pull request.)
Below, some examples where I consider that assignment expressions give
a value to the reader.
== Good: site (straighforward) ==
env_base = os.environ.get("PYTHONUSERBASE", None)
if env_base:
return env_base
replaced with:
if (env_base := os.environ.get("PYTHONUSERBASE", None)):
return env_base
Note: env_base is only used inside the if block.
== Good: datetime (more concise code) ==
New code:
def isoformat(self, timespec='auto'):
s = _format_time(self._hour, self._minute, self._second,
self._microsecond, timespec)
if (tz := self._tzstr()):
s += tz
return s
This example shows the benefit of the PEP 572: remove one line without
making the code worse to read.
== Good: logging.handlers ==
def close(self):
self.acquire()
try:
sock = self.sock
if sock:
self.sock = None
sock.close()
logging.Handler.close(self)
finally:
self.release()
replaced with:
def close(self):
self.acquire()
try:
if (sock := self.sock):
self.sock = None
sock.close()
logging.Handler.close(self)
finally:
self.release()
== Good: doctest ==
New code:
# Deal with exact matches possibly needed at one or both ends.
startpos, endpos = 0, len(got)
if (w := ws[0]): # starts with exact match
if got.startswith(w):
startpos = len(w)
del ws[0]
else:
return False
if (w := ws[-1]): # ends with exact match
if got.endswith(w):
endpos -= len(w)
del ws[-1]
else:
return False
...
This example is interesting: the 'w' variable is reused, but ":="
announces to the reader that the w is only intended to be used in one
if block.
== Good: csv (reuse var) ==
New code:
n = groupindex['quote'] - 1
if (key := m[n]):
quotes[key] = quotes.get(key, 0) + 1
try:
n = groupindex['delim'] - 1
key = m[n]
except KeyError:
continue
if key and (delimiters is None or key in delimiters):
delims[key] = delims.get(key, 0) + 1
As for doctest: "key := ..." shows that this value is only used in one
if block, but later key is reassigned to a new value.
== Good: difflib ==
New code using (isjunk := self.isjunk):
# Purge junk elements
self.bjunk = junk = set()
if (isjunk := self.isjunk):
for elt in b2j.keys():
if isjunk(elt):
junk.add(elt)
for elt in junk: # separate loop avoids separate list of keys
del b2j[elt]
-*-*-*-
== Borderline? sre_parse (two conditions) ==
code = CATEGORIES.get(escape)
if code and code[0] is IN:
return code
replaced with:
if (code := CATEGORIES.get(escape)) and code[0] is IN:
return code
The test "code[0] is IN" uses 'code' just after it's defined on the
same line. Maybe it is surprising me, since I'm not sure to assignment
expressions yet.
-*-*-*-
== BAD! argparse (use after if) ==
Ok, now let's see cases where I consider that assignment expressions
are inappropriate! Here is a first example.
help = self._root_section.format_help()
if help:
help = self._long_break_matcher.sub('\n\n', help)
help = help.strip('\n') + '\n'
return help
'help' is used after the if block.
== BAD! fileinput (use after if) ==
line = self._readline()
if line:
self._filelineno += 1
return line
if not self._file:
return line
'line' is used after the first if block.
== BAD! _osx_support (reuse var, use after if) ==
def _supports_universal_builds():
osx_version = _get_system_version()
if osx_version:
try:
osx_version = tuple(int(i) for i in osx_version.split('.'))
except ValueError:
osx_version = ''
return bool(osx_version >= (10, 4)) if osx_version else False
Surprising reusage of the 'osx_version' variable, using ":=" would
more the code even more confusing (and osx_version is used after the
if).
Victor
More information about the Python-Dev
mailing list