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