Create PEP 505 for None-aware operators
This commit is contained in:
parent
03d1f3be1a
commit
f960623a1e
|
@ -0,0 +1,199 @@
|
||||||
|
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:
|
Loading…
Reference in New Issue