[Python-ideas] Null coalescing operators
Steven D'Aprano
steve at pearwood.info
Sat Sep 19 07:06:48 CEST 2015
On Fri, Sep 18, 2015 at 05:49:36PM -0700, Andrew Barnert via Python-ideas wrote:
> Obviously "spam?" returns something with a __getattr__ method that
> just passes through to spam.__getattr__, except that on NoneType it
> returns something with a __getattr__ that always returns None. That
> solves the eggs case.
Ah, and now my enthusiasm for the whole idea is gone...
In my previous response, I imagined spam?.attr to be syntactic sugar for
`None if spam is None else spam.attr`. But having ? be an ordinary
operator that returns a special Null object feels too "Design Pattern-y"
to me. I think the Null design pattern is actually harmful, and I would
not like to see this proposal implemented this way.
(In another email, Andrew called the special object something like
NoneMaybe or NoneQuestion, I forget which. I'm going to call the object
Null, since that's less typing.)
The Null object pattern sounds like a great idea at first, but I find it
to be a code smell at best and outright harmful at worst. If you are
passing around an object which is conceptually None, but unlike None
reliably does nothing without raising an exception no matter what you do
with it, that suggests to me that something about your code is not
right.
If your functions already accept None, then you should just use None. If
they don't accept None, then why are you trying to smuggle None into
them using a quasi-None that unconditionally hides errors?
Here are some problems with the Null pattern as I see it:
(1) Suppose that spam? returns a special Null object, and Null.attr
itself returns Null. (As do Null[item] and Null(arg), of course.) This
matches the classic Null object design pattern, and gives us chaining
for free:
value = obj?.spam.eggs.cheese
But now `value` is Null, which may not be what we expect and may in fact
be a problem if we're expecting it to be "an actual value, or None"
rather than our quasi-None Null object.
Because `value` is now a Null, every time we pass it to a function, we
risk getting new Nulls in places that shouldn't get them. If a function
isn't expecting None, we should get an exception, but Null is designed
to not raise exceptions no matter what you do with it. So we risk
contaminating our data with Nulls in unexpected places.
Eventually, of course, there comes a time where we need to deal with the
actual value. With the Null pattern in place, we have to deal with two
special cases, not one:
# I assume Null is a singleton, otherwise use isinstance
if filename is not None and filename is not Null:
os.unlink(filename)
A small nuisance, to be sure, but part of the reason why I really don't
think much of the Null object pattern. It sounds good on paper, but I
think it's actually more dangerous and inconvenient than the problem it
tries to solve.
(2) We can avoid the worst of the Null design (anti-)pattern by having
Null.attr return None instead of Null. Unfortunately, that means we've
lost automatic chaining. If you have an object that might be None, we
have to explicitly use the ? operator after each lookup except the last:
value = obj?.spam?.eggs?.cheese
which is (a) messy, (b) potentially inefficient, and (c) potentially
hides subtle bugs.
Here is a scenario where it hides bugs. Suppose obj may be None, but if
it is not, then obj.spam *must* be a object with an eggs attribute. If
obj.spam is None, that's a bug that needs fixing. Suppose we start off
by writing the obvious thing:
obj?.spam.eggs
but that fails because obj=None raises an exception:
obj? returns Null
Null.spam returns None
None.eggs raises
So to protect against that, we might write:
obj?.spam?.eggs
but that protects against too much, and hides the fact that obj.spam
exists but is None.
As far as I am concerned, any use of a Null object has serious
downsides. If people want to explicitly use it in their own code, well,
good luck with that. I don't think Python should be making it a
built-in.
I think the first case, the classic Null design pattern, is actually
*better* because the downsides are anything but subtle, and people will
soon learn not to touch it with a 10ft pole *wink*, while the second
case, the "Null.attr gives None" case, is actually worse because it
isn't *obviously* wrong and can subtly hide bugs.
How does my earlier idea of ? as syntactic sugar compare with those?
In that case, there is no special Null object, there's only None. So we
avoid the risk of Null infection, and avoid needing to check specially
for Null. It also avoids the bug-hiding scenario:
obj?.spam.eggs.cheese
is equivalent to:
None if obj is None else obj.spam.eggs
If obj is None, we get None, as we expect. If it is not None, we get
obj.spam.eggs as we expect. If obj.spam is wrongly None, then we get an
exception, as we should.
--
Steve
More information about the Python-ideas
mailing list