[Python-ideas] PEP 505: None-aware operators
Steve Dower
steve.dower at python.org
Thu Jul 19 17:28:47 EDT 2018
Thanks everyone for the feedback and discussion so far. I want to
address some of the themes, so apologies for not quoting individuals and
for doing this in one post instead of twenty.
------
* "It looks like line noise"
Thanks for the feedback. There's nothing constructive for me to take
from this.
* "I've never needed this"
Also not very actionable, but as background I'll say that this was
exactly my argument against adding them to C#. But my coding style has
adapted to suit (for example, I'm more likely to use "null" as a default
value and have a single function implementation than two
mostly-duplicated overloads).
* "It makes it more complex"
* "It's harder to follow the flow"
Depends on your measure of complexity. For me, I prioritise "area under
the indentation" as my preferred complexity metric (more lines*indents
== more complex), as well as left-to-right reading of each line (more
random access == more complex). By these measures, ?. significantly
reduces the complexity over any of the current or future alternatives::
def f(a=None):
name = 'default'
if a is not None:
user = a.get_user()
if user is not None:
name = user.name
print(name)
def f(a=None):
if a is not None:
user = a.get_user()
name = user.name if user is not None else 'default'
print(name)
else
print('default')
def f(a=None):
user = a.get_user() if a is not None else None
name = user.name if user is not None else 'default'
print(name)
def f(a=None):
print(user.name
if (user := a.get_user() if a is not None else None) is not None
else 'default')
def f(a=None):
print(a?.get_user()?.name ?? 'none')
* "We have 'or', we don't need '??'"
Nearly-agreed, but I think the tighter binding on ?? makes it more
valuable and tighter test make it valuable in place of 'or'.
For example, compare:
a ** b() or 2 # actual: (a ** b()) or 2
a ** b() ?? 2 # proposed: a ** (b() ?? 2)
In the first, the presence of 'or' implies that either b() or __pow__(a,
b()) could return a non-True value. This is correct (it could return 0
if a == 0). And the current precedence results in the result of __pow__
being used for the check.
In the second one, the presence of the '??' implies that either b() or
__pow__(a, b()) could return None. The latter should never happen, and
so the choices are to make the built-in types propagate Nones when
passed None (uhh... no) or to make '??' bind to the closer part of the
expression.
(If you don't think it's likely enough that a function could return
[float, None], then assume 'a ** b?.c ?? 2' instead.)
* "We could have '||', we don't need '??'"
Perhaps, though this is basically just choosing the bikeshed colour. In
the absence of a stronger argument, matching existing languages
equivalent operators instead of operators that do different things in
those languages should win.
* "We could have 'else', we don't need '??'"
This is the "a else 'default'" rather than "a ?? 'default'" proposal,
which I do like the look of, but I think it will simultaneously mess
with operator precedence and also force me to search for the 'if' that
we actually need to be comparing "(a else 'default')" vs. "a ?? 'default'"::
x = a if b else c else d
x = a if (b else c) else d
x = a if b else (c else d)
* "It's not clear whether it's 'is not None' or 'hasattr' checks"
I'm totally sympathetic to this. Ultimately, like everything else, this
is a concept that has to be taught/learned rather than known intrinsically.
The main reasons for not having 'a?.b' be directly equivalent to
getattr(a, 'b', ???) is that you lose the easy ability to find typos,
and we also already have the getattr() approach.
(Aside: in this context, why should the result be 'None' if an attribute
is missing? For None, the None value propagates (getattr(a, 'b', a)),
while for falsies you could argue the same thing applies. But for a
silently handled AttributeError? You still have to make the case that
None is special here, just special as a return value vs. special as a test.)
* "The semantics of this example changed from getattr() with ?."
Yes, this was a poor example. On re-reading, all of the checks are
indeed looking for optional attributes, rather than looking them up on
optional targets. I'll find a better one (I've certainly seen and/or
written code like this that was intended to avoid crashing on None, but
I stopped my search of the stdlib too soon after finding this example).
* "Bitwise operators"
Uh... yeah. Have fun over there :)
* "Assumes the only falsie ever returned [in some context] is None"
I argue that it assumes the only falsie you want to replace with a
different value is None. In many cases, I'd expect the None to be
replaced with a falsie of the intended type:
x = maybe_get_int() ?? 0
y = maybe_get_list() ?? []
Particularly for the second case, if you are about to mutate the list,
then it could be very important that you don't replace the provided
reference with your own, just because it happens to be empty right now
(this is the logging example in the PEP). Supporting a default value in
the get() calls is obviously a better way to do this though, provided
you have the ability to modify the API.
But for me, the ?? operator makes its strongest case when you are
getting attributes from potentially None values:
x = maybe_get()?.get_int() ?? 0
y = maybe_get()?.get_list() ?? []
In this case, using 'or' may replace an intentionally falsie value with
your own, while using a default parameter is still going to leave you
with None if the first maybe_get() returned nothing.
"?." without "??" feels like a trap, while "??" without "?." feels
largely unnecessary. But both together lets you turn many lines of code
into a much shorter snippet that reads left-to-right and lets you assume
success until you reach the "??". That, for me, is peak readability.
More information about the Python-ideas
mailing list