200 lines
5.7 KiB
Plaintext
200 lines
5.7 KiB
Plaintext
|
PEP: 505
|
|||
|
Title: None coalescing operators
|
|||
|
Version: $Revision$
|
|||
|
Last-Modified: $Date$
|
|||
|
Author: Mark E. Haase <mehaase@gmail.com>
|
|||
|
Status: Draft
|
|||
|
Type: Standards Track
|
|||
|
Content-Type: text/x-rst
|
|||
|
Created: 18-Sep-2015
|
|||
|
Python-Version: 3.6
|
|||
|
|
|||
|
Abstract
|
|||
|
========
|
|||
|
|
|||
|
Several modern programming languages have so-called "null coalescing" or
|
|||
|
"null aware" operators, including C#, Dart, Perl, Swift, and PHP (starting in
|
|||
|
version 7). These operators provide syntactic sugar for common patterns
|
|||
|
involving null references. [1]_ [2]_
|
|||
|
|
|||
|
* The "null coalescing" operator is a binary operator that returns its first
|
|||
|
first non-null operand.
|
|||
|
* The "null aware member access" operator is a binary operator that accesses
|
|||
|
an instance member only if that instance is non-null. It returns null
|
|||
|
otherwise.
|
|||
|
* The "null aware index access" operator is a binary operator that accesses a
|
|||
|
member of a collection only if that collection is non-null. It returns null
|
|||
|
otherwise.
|
|||
|
|
|||
|
Python does not have any directly equivalent syntax. The ``or`` operator can
|
|||
|
be used to similar effect but checks for a truthy value, not ``None``
|
|||
|
specifically. The ternary operator ``... if ... else ...`` can be used for
|
|||
|
explicit null checks but is more verbose and typically duplicates part of the
|
|||
|
expression in between ``if`` and ``else``. The proposed ``None`` coalescing
|
|||
|
and ``None`` aware operators ofter an alternative syntax that is more
|
|||
|
intuitive and concise.
|
|||
|
|
|||
|
|
|||
|
Rationale
|
|||
|
=========
|
|||
|
|
|||
|
Null Coalescing Operator
|
|||
|
------------------------
|
|||
|
|
|||
|
The following code illustrates how the ``None`` coalescing operators would
|
|||
|
work in Python::
|
|||
|
|
|||
|
>>> title = 'My Title'
|
|||
|
>>> title ?? 'Default Title'
|
|||
|
'My Title'
|
|||
|
>>> title = None
|
|||
|
>>> title ?? 'Bar'
|
|||
|
'Default Title'
|
|||
|
|
|||
|
Similar behavior can be achieved with the ``or`` operator, but ``or`` checks
|
|||
|
whether its left operand is false-y, not specifically ``None``. This can lead
|
|||
|
to surprising behavior. Consider the scenario of computing the price of some
|
|||
|
products a customer has in his/her shopping cart::
|
|||
|
|
|||
|
>>> price = 100
|
|||
|
>>> requested_quantity = 5
|
|||
|
>>> default_quantity = 1
|
|||
|
>>> (requested_quantity or default_quantity) * price
|
|||
|
500
|
|||
|
>>> requested_quantity = None
|
|||
|
>>> (requested_quantity or default_quantity) * price
|
|||
|
100
|
|||
|
>>> requested_quantity = 0
|
|||
|
>>> (requested_quantity or default_quantity) * price # oops!
|
|||
|
100
|
|||
|
|
|||
|
This type of bug is not possible with the ``None`` coalescing operator,
|
|||
|
because there is no implicit type coersion to ``bool``::
|
|||
|
|
|||
|
>>> price = 100
|
|||
|
>>> requested_quantity = 0
|
|||
|
>>> default_quantity = 1
|
|||
|
>>> (requested_quantity ?? default_quantity) * price
|
|||
|
0
|
|||
|
|
|||
|
The same correct behavior can be achieved with the ternary operator. Here is
|
|||
|
an excerpt from the popular Requests package::
|
|||
|
|
|||
|
data = [] if data is None else data
|
|||
|
files = [] if files is None else files
|
|||
|
headers = {} if headers is None else headers
|
|||
|
params = {} if params is None else params
|
|||
|
hooks = {} if hooks is None else hooks
|
|||
|
|
|||
|
This particular formulation has the undesirable effect of putting the operands
|
|||
|
in an unintuitive order: the brain thinks, "use ``data`` if possible and use
|
|||
|
``[]`` as a fallback," but the code puts the fallback _before_ the preferred
|
|||
|
value.
|
|||
|
|
|||
|
The author of this package could have written it like this instead::
|
|||
|
|
|||
|
data = data if data is not None else []
|
|||
|
files = files if files is not None else []
|
|||
|
headers = headers if headers is not None else {}
|
|||
|
params = params if params is not None else {}
|
|||
|
hooks = hooks if hooks is not None else {}
|
|||
|
|
|||
|
This ordering of the operands is more intuitive, but it requires 4 extra
|
|||
|
characters (for "not "). It also highlights the repetition of identifiers:
|
|||
|
``data if data``, ``files if files``, etc. The ``None`` coalescing operator
|
|||
|
improves readability::
|
|||
|
|
|||
|
data = data ?? []
|
|||
|
files = files ?? []
|
|||
|
headers = headers ?? {}
|
|||
|
params = params ?? {}
|
|||
|
hooks = hooks ?? {}
|
|||
|
|
|||
|
The ``None`` coalescing operator also has a corresponding assignment shortcut.
|
|||
|
|
|||
|
::
|
|||
|
|
|||
|
data ??= []
|
|||
|
files ??= []
|
|||
|
headers ??= {}
|
|||
|
params ??= {}
|
|||
|
hooks ??= {}
|
|||
|
|
|||
|
The ``None`` coalescing operator is left-associative, which allows for easy
|
|||
|
chaining::
|
|||
|
|
|||
|
>>> user_title = None
|
|||
|
>>> local_default_title = None
|
|||
|
>>> global_default_title = 'Global Default Title'
|
|||
|
>>> title = user_title ?? local_default_title ?? global_default_title
|
|||
|
'My Title'
|
|||
|
|
|||
|
The direction of associativity is important because the ``None`` coalesing
|
|||
|
operator short circuits: if it's left operand is non-null, then the right
|
|||
|
operand is not evaluated.
|
|||
|
|
|||
|
>>> def get_default(): raise Exception()
|
|||
|
>>> 'My Title' ?? get_default()
|
|||
|
'My Title'
|
|||
|
|
|||
|
|
|||
|
Null-Aware Member Access Operator
|
|||
|
---------------------------------
|
|||
|
|
|||
|
>>> title = 'My Title'
|
|||
|
>>> title.upper()
|
|||
|
'MY TITLE'
|
|||
|
>>> title = None
|
|||
|
>>> title.upper()
|
|||
|
Traceback (most recent call last):
|
|||
|
File "<stdin>", line 1, in <module>
|
|||
|
AttributeError: 'NoneType' object has no attribute 'upper'
|
|||
|
>>> title?.upper()
|
|||
|
None
|
|||
|
|
|||
|
|
|||
|
Null-Aware Index Access Operator
|
|||
|
---------------------------------
|
|||
|
|
|||
|
>>> person = {'name': 'Mark', 'age': 32}
|
|||
|
>>> person['name']
|
|||
|
'Mark'
|
|||
|
>>> person = None
|
|||
|
>>> person['name']
|
|||
|
Traceback (most recent call last):
|
|||
|
File "<stdin>", line 1, in <module>
|
|||
|
TypeError: 'NoneType' object is not subscriptable
|
|||
|
>>> person?['name']
|
|||
|
None
|
|||
|
|
|||
|
|
|||
|
Specification
|
|||
|
=============
|
|||
|
|
|||
|
|
|||
|
References
|
|||
|
==========
|
|||
|
|
|||
|
.. [1] Wikipedia: Null coalescing operator
|
|||
|
(https://en.wikipedia.org/wiki/Null_coalescing_operator)
|
|||
|
|
|||
|
.. [2] Seth Ladd's Blog: Null-aware operators in Dart
|
|||
|
(http://blog.sethladd.com/2015/07/null-aware-operators-in-dart.html)
|
|||
|
|
|||
|
|
|||
|
Copyright
|
|||
|
=========
|
|||
|
|
|||
|
This document has been placed in the public domain.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
..
|
|||
|
Local Variables:
|
|||
|
mode: indented-text
|
|||
|
indent-tabs-mode: nil
|
|||
|
sentence-end-double-space: t
|
|||
|
fill-column: 70
|
|||
|
coding: utf-8
|
|||
|
End:
|