[Python-ideas] PEP 505: None-aware operators

David Mertz mertz at gnosis.cx
Wed Jul 18 20:05:56 EDT 2018


As before, I'm a strong -1 on this.

I recognize the semantic utility (albeit, I've needed it much less than
some folks testify). But the obscure characters are accumulating far too
fast with this.

Even with PEP 572, I would have preferred 'as' and restrictions on where
it's allowed, but ':=' is more familiar from numerous languages and
pseudo-code notations.  '??' and '?.' and ?[]' are really just marching
into APL and Perl territory. Yes, I know such operators exist in other
languages, but it feels very unpythonic.

On Wed, Jul 18, 2018, 1:46 PM Steve Dower <steve.dower at python.org> wrote:

> Possibly this is exactly the wrong time to propose the next big syntax
> change, since we currently have nobody to declare on it, but since we're
> likely to argue for a while anyway it probably can't hurt (and maybe
> this will become the test PEP for whoever takes the reins?).
>
> FWIW, Guido had previously indicated that he was generally favourable
> towards most of this proposal, provided we could figure out coherent
> semantics. Last time we tried, that didn't happen, so this time I've
> made the semantics much more precise, have implemented and verified
> them, and made much stronger statements about why we are proposing these.
>
> Additional thanks to Mark Haase for writing most of the PEP. All the
> fair and balanced parts are his - all the overly strong opinions are mine.
>
> Also thanks to Nick Coghlan for writing PEPs 531 and 532 last time we
> went through this - if you're unhappy with "None" being treated as a
> special kind of value, I recommend reading those before you start
> repeating them.
>
> There is a formatted version of this PEP at
> https://www.python.org/dev/peps/pep-0505/
>
> My current implementation is at
> https://github.com/zooba/cpython/tree/pep-505 (though I'm considering
> removing some of the new opcodes I added and just generating more
> complex code - in any case, let's get hung up on the proposal rather
> than the implementation :) )
>
> Let the discussions begin!
>
> ---
>
> PEP: 505
> Title: None-aware operators
> Version: $Revision$
> Last-Modified: $Date$
> Author: Mark E. Haase <mehaase at gmail.com>, Steve Dower
> <steve.dower at python.org>
> Status: Draft
> Type: Standards Track
> Content-Type: text/x-rst
> Created: 18-Sep-2015
> Python-Version: 3.8
>
> Abstract
> ========
>
> Several modern programming languages have so-called "``null``-coalescing"
> or
> "``null``- aware" operators, including C# [1]_, Dart [2]_, Perl, Swift,
> and PHP
> (starting in version 7). These operators provide syntactic sugar for common
> patterns involving null references.
>
> * The "``null``-coalescing" operator is a binary operator that returns
> its left
>    operand if it is not ``null``. Otherwise it returns its right operand.
> * The "``null``-aware member access" operator accesses an instance
> member only
>    if that instance is non-``null``. Otherwise it returns ``null``.
> (This is also
>    called a "safe navigation" operator.)
> * The "``null``-aware index access" operator accesses an element of a
> collection
>    only if that collection is non-``null``. Otherwise it returns
> ``null``. (This
>    is another type of "safe navigation" operator.)
>
> This PEP proposes three ``None``-aware operators for Python, based on the
> definitions and other language's implementations of those above.
> Specifically:
>
> * The "``None`` coalescing`` binary operator ``??`` returns the left
> hand side
>    if it evaluates to a value that is not ``None``, or else it evaluates
> and
>    returns the right hand side. A coalescing ``??=`` augmented assignment
>    operator is included.
> * The "``None``-aware attribute access" operator ``?.`` evaluates the
> complete
>    expression if the left hand side evaluates to a value that is not
> ``None``
> * The "``None``-aware indexing" operator ``?[]`` evaluates the complete
>    expression if the left hand site evaluates to a value that is not
> ``None``
>
> Syntax and Semantics
> ====================
>
> Specialness of ``None``
> -----------------------
>
> The ``None`` object denotes the lack of a value. For the purposes of these
> operators, the lack of a value indicates that the remainder of the
> expression
> also lacks a value and should not be evaluated.
>
> A rejected proposal was to treat any value that evaluates to false in a
> Boolean context as not having a value. However, the purpose of these
> operators
> is to propagate the "lack of value" state, rather that the "false" state.
>
> Some argue that this makes ``None`` special. We contend that ``None`` is
> already special, and that using it as both the test and the result of these
> operators does not change the existing semantics in any way.
>
> See the `Rejected Ideas`_ section for discussion on the rejected
> approaches.
>
> Grammar changes
> ---------------
>
> The following rules of the Python grammar are updated to read::
>
>      augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' |
> '^=' |
>                  '<<=' | '>>=' | '**=' | '//=' | '??=')
>
>      power: coalesce ['**' factor]
>      coalesce: atom_expr ['??' factor]
>      atom_expr: ['await'] atom trailer*
>      trailer: ('(' [arglist] ')' |
>                '[' subscriptlist ']' |
>                '?[' subscriptlist ']' |
>                '.' NAME |
>                '?.' NAME)
>
> Inserting the ``coalesce`` rule in this location ensures that expressions
> resulting in ``None`` are natuarlly coalesced before they are used in
> operations that would typically raise ``TypeError``. Like ``and`` and
> ``or``
> the right-hand expression is not evaluated until the left-hand side is
> determined to be ``None``. For example::
>
>      a, b = None, None
>      def c(): return None
>      def ex(): raise Exception()
>
>      (a ?? 2 ** b ?? 3) == a ?? (2 ** (b ?? 3))
>      (a * b ?? c // d) == a * (b ?? c) // d
>      (a ?? True and b ?? False) == (a ?? True) and (b ?? False)
>      (c() ?? c() ?? True) == True
>      (True ?? ex()) == True
>      (c ?? ex)() == c()
>
> Augmented coalescing assignment only rebinds the name if its current
> value is
> ``None``. If the target name already has a value, the right-hand side is
> not
> evaluated. For example::
>
>      a = None
>      b = ''
>      c = 0
>
>      a ??= 'value'
>      b ??= undefined_name
>      c ??= shutil.rmtree('/')    # don't try this at home, kids
>
>      assert a == 'value'
>      assert b == ''
>      assert c == '0' and any(os.scandir('/'))
>
> Adding new trailers for the other ``None``-aware operators ensures that
> they
> may be used in all valid locations for the existing equivalent operators,
> including as part of an assignment target (more details below). As the
> existing
> evaluation rules are not directly embedded in the grammar, we specify the
> required changes here.
>
> Assume that the ``atom`` is always successfully evaluated. Each
> ``trailer`` is
> then evaluated from left to right, applying its own parameter (either its
> arguments, subscripts or attribute name) to produce the value for the next
> ``trailer``. Finally, if present, ``await`` is applied.
>
> For example, ``await a.b(c).d[e]`` is currently parsed as
> ``['await', 'a', '.b', '(c)', '.d', '[e]']`` and evaluated::
>
>      _v = a
>      _v = _v.b
>      _v = _v(c)
>      _v = _v.d
>      _v = _v[e]
>      await _v
>
> When a ``None``-aware operator is present, the left-to-right evaluation
> may be
> short-circuited. For example, ``await a?.b(c).d?[e]`` is evaluated::
>
>      _v = a
>      if _v is not None:
>          _v = _v.b
>          _v = _v(c)
>          _v = _v.d
>          if _v is not None:
>              _v = _v[e]
>      await _v
>
> .. note::
>      ``await`` will almost certainly fail in this context, as it would in
>      the case where code attempts ``await None``. We are not proposing
> to add a
>      ``None``-aware ``await`` keyword here, and merely include it in this
>      example for completeness of the specification, since the ``atom_expr``
>      grammar rule includes the keyword. If it were in its own rule, we
> would have
>      never mentioned it.
>
> Parenthesised expressions are handled by the ``atom`` rule (not shown
> above),
> which will implicitly terminate the short-circuiting behaviour of the above
> transformation. For example, ``(a?.b ?? c).d?.e`` is evaluated as::
>
>      # a?.b
>      _v = a
>      if _v is not None:
>          _v = _v.b
>
>      # ... ?? c
>      if _v is None:
>          _v = c
>
>      # (...).d?.e
>      _v = _v.d
>      if _v is not None:
>          _v = _v.e
>
> When used as an assignment target, the ``None``-aware operations may only
> be
> used in a "load" context. That is, ``a?.b = 1`` and ``a?[b] = 1`` will
> raise
> ``SyntaxError``. Use earlier in the expression (``a?.b.c = 1``) is
> permitted,
> though unlikely to be useful unless combined with a coalescing operation::
>
>      (a?.b ?? d).c = 1
>
>
> Examples
> ========
>
> This section presents some examples of common ``None`` patterns and
> shows what
> conversion to use ``None``-aware operators may look like.
>
> Standard Library
> ----------------
>
> Using the ``find-pep505.py`` script[3]_ an analysis of the Python 3.7
> standard
> library discovered up to 678 code snippets that could be replaced with
> use of
> one of the ``None``-aware operators::
>
>      $ find /usr/lib/python3.7 -name '*.py' | xargs python3.7
> find-pep505.py
>      <snip>
>      Total None-coalescing `if` blocks: 449
>      Total [possible] None-coalescing `or`: 120
>      Total None-coalescing ternaries: 27
>      Total Safe navigation `and`: 13
>      Total Safe navigation `if` blocks: 61
>      Total Safe navigation ternaries: 8
>
> Some of these are shown below as examples before and after converting to
> use the
> new operators.
>
>  From ``bisect.py``::
>
>      def insort_right(a, x, lo=0, hi=None):
>          # ...
>          if hi is None:
>              hi = len(a)
>          # ...
>
> After updating to use the ``??=`` augmented assignment statement::
>
>      def insort_right(a, x, lo=0, hi=None):
>          # ...
>          hi ??= len(a)
>          # ...
>
>  From ``calendar.py``::
>
>      encoding = options.encoding
>      if encoding is None:
>          encoding = sys.getdefaultencoding()
>      optdict = dict(encoding=encoding, css=options.css)
>
> After updating to use the ``??`` operator::
>
>      optdict = dict(encoding=encoding ?? sys.getdefaultencoding(),
>                     css=options.css)
>
>  From ``dis.py``::
>
>      def _get_const_info(const_index, const_list):
>          argval = const_index
>          if const_list is not None:
>              argval = const_list[const_index]
>          return argval, repr(argval)
>
> After updating to use the ``?[]`` and ``??`` operators::
>
>      def _get_const_info(const_index, const_list):
>          argval = const_list?[const_index] ?? const_index
>          return argval, repr(argval)
>
>  From ``inspect.py``::
>
>      for base in object.__bases__:
>          for name in getattr(base, "__abstractmethods__", ()):
>              value = getattr(object, name, None)
>              if getattr(value, "__isabstractmethod__", False):
>                  return True
>
> After updating to use the ``?.`` operator (and deliberately not
> converting to
> use ``any()``)::
>
>      for base in object.__bases__:
>          for name in base?.__abstractmethods__ ?? ():
>              if object?.name?.__isabstractmethod__:
>                  return True
>
>  From ``os.py``::
>
>      if entry.is_dir():
>          dirs.append(name)
>          if entries is not None:
>              entries.append(entry)
>      else:
>          nondirs.append(name)
>
> After updating to use the ``?.`` operator::
>
>      if entry.is_dir():
>          dirs.append(name)
>          entries?.append(entry)
>      else:
>          nondirs.append(name)
>
>
> jsonify
> -------
>
> This example is from a Python web crawler that uses the Flask framework
> as its
> front-end. This function retrieves information about a web site from a SQL
> database and formats it as JSON to send to an HTTP client::
>
>      class SiteView(FlaskView):
>          @route('/site/<id_>', methods=['GET'])
>          def get_site(self, id_):
>              site = db.query('site_table').find(id_)
>
>              return jsonify(
>                  first_seen=site.first_seen.isoformat() if
> site.first_seen is not None else None,
>                  id=site.id,
>                  is_active=site.is_active,
>                  last_seen=site.last_seen.isoformat() if site.last_seen
> is not None else None,
>                  url=site.url.rstrip('/')
>              )
>
> Both ``first_seen`` and ``last_seen`` are allowed to be ``null`` in the
> database, and they are also allowed to be ``null`` in the JSON response.
> JSON
> does not have a native way to represent a ``datetime``, so the server's
> contract
> states that any non-``null`` date is represented as an ISO-8601 string.
>
> Without knowing the exact semantics of the ``first_seen`` and ``last_seen``
> attributes, it is impossible to know whether the attribute can be safely or
> performantly accessed multiple times.
>
> One way to fix this code is to replace each conditional expression with an
> explicit value assignment and a full ``if``/``else`` block::
>
>      class SiteView(FlaskView):
>          @route('/site/<id_>', methods=['GET'])
>          def get_site(self, id_):
>              site = db.query('site_table').find(id_)
>
>              first_seen_dt = site.first_seen
>              if first_seen_dt is None:
>                  first_seen = None
>              else:
>                  first_seen = first_seen_dt.isoformat()
>
>              last_seen_dt = site.last_seen
>              if last_seen_dt is None:
>                  last_seen = None
>              else:
>                  last_seen = last_seen_dt.isoformat()
>
>              return jsonify(
>                  first_seen=first_seen,
>                  id=site.id,
>                  is_active=site.is_active,
>                  last_seen=last_seen,
>                  url=site.url.rstrip('/')
>              )
>
> This adds ten lines of code and four new code paths to the function,
> dramatically increasing the apparent complexity. Rewriting using the
> ``None``-aware attribute operator results in shorter code with more clear
> intent::
>
>      class SiteView(FlaskView):
>          @route('/site/<id_>', methods=['GET'])
>          def get_site(self, id_):
>              site = db.query('site_table').find(id_)
>
>              return jsonify(
>                  first_seen=site.first_seen?.isoformat(),
>                  id=site.id,
>                  is_active=site.is_active,
>                  last_seen=site.last_seen?.isoformat(),
>                  url=site.url.rstrip('/')
>              )
>
> Grab
> ----
>
> The next example is from a Python scraping library called `Grab
> <
> https://github.com/lorien/grab/blob/4c95b18dcb0fa88eeca81f5643c0ebfb114bf728/gr
> ab/upload.py
> <https://github.com/lorien/grab/blob/4c95b18dcb0fa88eeca81f5643c0ebfb114bf728/grab/upload.py>
> >`_::
>
>      class BaseUploadObject(object):
>          def find_content_type(self, filename):
>              ctype, encoding = mimetypes.guess_type(filename)
>              if ctype is None:
>                  return 'application/octet-stream'
>              else:
>                  return ctype
>
>      class UploadContent(BaseUploadObject):
>          def __init__(self, content, filename=None, content_type=None):
>              self.content = content
>              if filename is None:
>                  self.filename = self.get_random_filename()
>              else:
>                  self.filename = filename
>              if content_type is None:
>                  self.content_type = self.find_content_type(self.filename)
>              else:
>                  self.content_type = content_type
>
>      class UploadFile(BaseUploadObject):
>          def __init__(self, path, filename=None, content_type=None):
>              self.path = path
>              if filename is None:
>                  self.filename = os.path.split(path)[1]
>              else:
>                  self.filename = filename
>              if content_type is None:
>                  self.content_type = self.find_content_type(self.filename)
>              else:
>                  self.content_type = content_type
>
> This example contains several good examples of needing to provide default
> values. Rewriting to use conditional expressions reduces the overall
> lines of
> code, but does not necessarily improve readability::
>
>      class BaseUploadObject(object):
>          def find_content_type(self, filename):
>              ctype, encoding = mimetypes.guess_type(filename)
>              return 'application/octet-stream' if ctype is None else ctype
>
>      class UploadContent(BaseUploadObject):
>          def __init__(self, content, filename=None, content_type=None):
>              self.content = content
>              self.filename = (self.get_random_filename() if filename
>                  is None else filename)
>              self.content_type = (self.find_content_type(self.filename)
>                  if content_type is None else content_type)
>
>      class UploadFile(BaseUploadObject):
>          def __init__(self, path, filename=None, content_type=None):
>              self.path = path
>              self.filename = (os.path.split(path)[1] if filename is
>                  None else filename)
>              self.content_type = (self.find_content_type(self.filename)
>                  if content_type is None else content_type)
>
> The first ternary expression is tidy, but it reverses the intuitive order
> of
> the operands: it should return ``ctype`` if it has a value and use the
> string
> literal as fallback. The other ternary expressions are unintuitive and so
> long that they must be wrapped. The overall readability is worsened, not
> improved.
>
> Rewriting using the ``None`` coalescing operator::
>
>      class BaseUploadObject(object):
>          def find_content_type(self, filename):
>              ctype, encoding = mimetypes.guess_type(filename)
>              return ctype ?? 'application/octet-stream'
>
>      class UploadContent(BaseUploadObject):
>          def __init__(self, content, filename=None, content_type=None):
>              self.content = content
>              self.filename = filename ?? self.get_random_filename()
>              self.content_type = content_type ??
> self.find_content_type(self.filename)
>
>      class UploadFile(BaseUploadObject):
>          def __init__(self, path, filename=None, content_type=None):
>              self.path = path
>              self.filename = filename ?? os.path.split(path)[1]
>              self.content_type = content_type ??
> self.find_content_type(self.filename)
>
> This syntax has an intuitive ordering of the operands. In
> ``find_content_type``,
> for example, the preferred value ``ctype`` appears before the fallback
> value.
> The terseness of the syntax also makes for fewer lines of code and less
> code to
> visually parse, and reading from left-to-right and top-to-bottom more
> accurately
> follows the execution flow.
>
>
> Rejected Ideas
> ==============
>
> The first three ideas in this section are oft-proposed alternatives to
> treating
> ``None`` as special. For further background on why these are rejected,
> see their
> treatment in `PEP 531 <https://www.python.org/dev/peps/pep-0531/>`_ and
> `PEP 532 <https://www.python.org/dev/peps/pep-0532/>`_ and the associated
> discussions.
>
> No-Value Protocol
> -----------------
>
> The operators could be generalised to user-defined types by defining a
> protocol
> to indicate when a value represents "no value". Such a protocol may be a
> dunder
> method ``__has_value__(self)` that returns ``True`` if the value should be
> treated as having a value, and ``False`` if the value should be treated
> as no
> value.
>
> With this generalization, ``object`` would implement a dunder method
> equivalent
> to this::
>
>      def __has_value__(self):
>          return True
>
> ``NoneType`` would implement a dunder method equivalent to this::
>
>      def __has_value__(self):
>          return False
>
> In the specification section, all uses of ``x is None`` would be
> replaced with
> ``not x.__has_value__()``.
>
> This generalization would allow for domain-specific "no-value" objects to
> be
> coalesced just like ``None``. For example the ``pyasn1`` package has a type
> called ``Null`` that represents an ASN.1 ``null``::
>
>      >>> from pyasn1.type import univ
>      >>> univ.Null() ?? univ.Integer(123)
>      Integer(123)
>
> Similarly, values such as ``math.nan`` and ``NotImplemented`` could be
> treated
> as representing no value.
>
> However, the "no-value" nature of these values is domain-specific, which
> means
> they *should* be treated as a value by the language. For example,
> ``math.nan.imag`` is well defined (it's ``0.0``), and so short-circuiting
> ``math.nan?.imag`` to return ``math.nan`` would be incorrect.
>
> As ``None`` is already defined by the language as being the value that
> represents "no value", and the current specification would not preclude
> switching to a protocol in the future (though changes to built-in
> objects would
> not be compatible), this idea is rejected for now.
>
> Boolean-aware operators
> -----------------------
>
> This suggestion is fundamentally the same as adding a no-value protocol,
> and so
> the discussion above also applies.
>
> Similar behavior to the ``??`` operator can be achieved with an ``or``
> expression, however ``or`` checks whether its left operand is false-y
> and not
> specifically ``None``. This approach is attractive, as it requires fewer
> changes
> to the language, but ultimately does not solve the underlying problem
> correctly.
>
> Assuming the check is for truthiness rather than ``None``, there is no
> longer a
> need for the ``??`` operator. However, applying this check to the ``?.``
> and
> ``?[]`` operators prevents perfectly valid operations applying
>
> Consider the following example, where ``get_log_list()`` may return either
> a
> list containing current log messages (potentially empty), or ``None`` if
> logging
> is not enabled::
>
>      lst = get_log_list()
>      lst?.append('A log message')
>
> If ``?.`` is checking for true values rather than specifically ``None``
> and the
> log has not been initialized with any items, no item will ever be
> appended. This
> violates the obvious intent of the code, which is to append an item. The
> ``append`` method is available on an empty list, as are all other list
> methods,
> and there is no reason to assume that these members should not be used
> because
> the list is presently empty.
>
> Further, there is no sensible result to use in place of the expression. A
> normal ``lst.append`` returns ``None``, but under this idea
> ``lst?.append`` may
> result in either ``[]`` or ``None``, depending on the value of ``lst``.
> As with
> the examples in the previous section, this makes no sense.
>
> As checking for truthiness rather than ``None`` results in apparently valid
> expressions no longer executing as intended, this idea is rejected.
>
> Exception-aware operators
> -------------------------
>
> Arguably, the reason to short-circuit an expression when ``None`` is
> encountered
> is to avoid the ``AttributeError`` or ``TypeError`` that would be raised
> under
> normal circumstances. As an alternative to testing for ``None``, the
> ``?.`` and
> ``?[]`` operators could instead handle ``AttributeError`` and ``TypeError``
> raised by the operation and skip the remainder of the expression.
>
> This produces a transformation for ``a?.b.c?.d.e`` similar to this::
>
>      _v = a
>      try:
>          _v = _v.b
>      except AttributeError:
>          pass
>      else:
>          _v = _v.c
>          try:
>              _v = _v.d
>          except AttributeError:
>              pass
>          else:
>              _v = _v.e
>
> One open question is which value should be returned as the expression
> when an
> exception is handled. The above example simply leaves the partial
> result, but
> this is not helpful for replacing with a default value. An alternative
> would be
> to force the result to ``None``, which then raises the question as to why
> ``None`` is special enough to be the result but not special enough to be
> the
> test.
>
> Secondly, this approach masks errors within code executed implicitly as
> part of
> the expression. For ``?.``, any ``AttributeError`` within a property or
> ``__getattr__`` implementation would be hidden, and similarly for
> ``?[]`` and
> ``__getitem__`` implementations.
>
> Similarly, simple typing errors such as ``{}?.ietms()`` could go unnoticed.
>
> Existing conventions for handling these kinds of errors in the form of the
> ``getattr`` builtin and the ``.get(key, default)`` method pattern
> established by
> ``dict`` show that it is already possible to explicitly use this behaviour.
>
> As this approach would hide errors in code, it is rejected.
>
> ``None``-aware Function Call
> ----------------------------
>
> The ``None``-aware syntax applies to attribute and index access, so it
> seems
> natural to ask if it should also apply to function invocation syntax. It
> might
> be written as ``foo?()``, where ``foo`` is only called if it is not None.
>
> This has been deferred on the basis of the proposed operators being
> intended
> to aid traversal of partially populated hierarchical data structures, *not*
> for traversal of arbitrary class hierarchies. This is reflected in the fact
> that none of the other mainstream languages that already offer this syntax
> have found it worthwhile to support a similar syntax for optional function
> invocations.
>
> A workaround similar to that used by C# would be to write
> ``maybe_none?.__call__(arguments)``. If the callable is ``None``, the
> expression will not be evaluated. (The C# equivalent uses ``?.Invoke()``
> on its
> callable type.)
>
> ``?`` Unary Postfix Operator
> ----------------------------
>
> To generalize the ``None``-aware behavior and limit the number of new
> operators
> introduced, a unary, postfix operator spelled ``?`` was suggested. The
> idea is
> that ``?`` might return a special object that could would override dunder
> methods that return ``self``. For example, ``foo?`` would evaluate to
> ``foo`` if
> it is not ``None``, otherwise it would evaluate to an instance of
> ``NoneQuestion``::
>
>      class NoneQuestion():
>          def __call__(self, *args, **kwargs):
>              return self
>
>          def __getattr__(self, name):
>              return self
>
>          def __getitem__(self, key):
>              return self
>
>
> With this new operator and new type, an expression like ``foo?.bar[baz]``
> evaluates to ``NoneQuestion`` if ``foo`` is None. This is a nifty
> generalization, but it's difficult to use in practice since most
> existing code
> won't know what ``NoneQuestion`` is.
>
> Going back to one of the motivating examples above, consider the
> following::
>
>      >>> import json
>      >>> created = None
>      >>> json.dumps({'created': created?.isoformat()})``
>
> The JSON serializer does not know how to serialize ``NoneQuestion``, nor
> will
> any other API. This proposal actually requires *lots of specialized logic*
> throughout the standard library and any third party library.
>
> At the same time, the ``?`` operator may also be **too general**, in the
> sense
> that it can be combined with any other operator. What should the following
> expressions mean?::
>
>      >>> x? + 1
>      >>> x? -= 1
>      >>> x? == 1
>      >>> ~x?
>
> This degree of generalization is not useful. The operators actually
> proposed
> herein are intentionally limited to a few operators that are expected to
> make it
> easier to write common code patterns.
>
> Built-in ``maybe``
> ------------------
>
> Haskell has a concept called `Maybe <https://wiki.haskell.org/Maybe>`_
> that
> encapsulates the idea of an optional value without relying on any special
> keyword (e.g. ``null``) or any special instance (e.g. ``None``). In
> Haskell, the
> purpose of ``Maybe`` is to avoid separate handling of "something" and
> nothing".
>
> A Python package called `pymaybe <https://pypi.org/p/pymaybe/>`_ provides
> a
> rough approximation. The documentation shows the following example::
>
>      >>> maybe('VALUE').lower()
>      'value'
>
>      >>> maybe(None).invalid().method().or_else('unknown')
>      'unknown'
>
> The function ``maybe()`` returns either a ``Something`` instance or a
> ``Nothing`` instance. Similar to the unary postfix operator described in
> the
> previous section, ``Nothing`` overrides dunder methods in order to allow
> chaining on a missing value.
>
> Note that ``or_else()`` is eventually required to retrieve the
> underlying value
> from ``pymaybe``'s wrappers. Furthermore, ``pymaybe`` does not short
> circuit any
> evaluation. Although ``pymaybe`` has some strengths and may be useful in
> its own
> right, it also demonstrates why a pure Python implementation of
> coalescing is
> not nearly as powerful as support built into the language.
>
> The idea of adding a builtin ``maybe`` type to enable this scenario is
> rejected.
>
> Just use a conditional expression
> ---------------------------------
>
> Another common way to initialize default values is to use the ternary
> operator.
> Here is an excerpt from the popular `Requests package
> <
> https://github.com/kennethreitz/requests/blob/14a555ac716866678bf17e43e23230d81
> a8149f5/requests/models.py#L212
> <https://github.com/kennethreitz/requests/blob/14a555ac716866678bf17e43e23230d81a8149f5/requests/models.py#L212>
> >`_::
>
>      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.
>
> When written using the ``None`` coalescing operator, the sample reads::
>
>      data = data ?? []
>      files = files ?? []
>      headers = headers ?? {}
>      params = params ?? {}
>      hooks = hooks ?? {}
>
>
> References
> ==========
>
> .. [1] C# Reference: Operators
>     (https://msdn.microsoft.com/en-us/library/6a71f45d.aspx)
>
> .. [2] A Tour of the Dart Language: Operators
>     (https://www.dartlang.org/docs/dart-up-and-running/ch02.html#operators
> )
>
> .. [3] Associated scripts
>     (https://github.com/python/peps/tree/master/pep-0505/)
>
> Copyright
> =========
>
> This document has been placed in the public domain.
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20180718/13532e3b/attachment-0001.html>


More information about the Python-ideas mailing list