Skip to content

Commit

Permalink
Incorporated open issues resolutions to the rest of the document.
Browse files Browse the repository at this point in the history
  • Loading branch information
facundobatista committed Jan 19, 2025
1 parent bf17fad commit b46a97f
Showing 1 changed file with 63 additions and 65 deletions.
128 changes: 63 additions & 65 deletions peps/pep-0769.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PEP: 769
Title: Add a 'default' keyword argument to 'attrgetter' and 'itemgetter'
Title: Add a 'default' keyword argument to 'attrgetter', 'itemgetter' and 'getitem'
Author: Facundo Batista <facundo@taniquetil.com.ar>
Discussions-To: https://discuss.python.org/t/76419
Status: Draft
Expand All @@ -12,8 +12,8 @@ Abstract
========

This proposal aims to enhance the :mod:`operator` module by adding a
``default`` keyword argument to the ``attrgetter`` and ``itemgetter``
functions. This addition would allow these functions to return a
``default`` keyword argument to the ``attrgetter``, ``itemgetter`` and
``getitem`` functions. This addition would allow these functions to return a
specified default value when the targeted attribute or item is missing,
thereby preventing exceptions and simplifying code that handles optional
attributes or items.
Expand All @@ -31,6 +31,10 @@ Introducing a ``default`` parameter would streamline operations involving
optional attributes or items, reducing boilerplate code and enhancing
code clarity.

A similar situation occurs with ``getitem``, with the added nuance that
allowing the specification of a default value in this case would resolve
a longstanding asymmetry with the :func:`getattr` built-in function.


Rationale
=========
Expand Down Expand Up @@ -58,11 +62,16 @@ Proposed behaviors:
- **itemgetter**: ``f = itemgetter(2, default=XYZ)`` followed by
``f(obj)`` would return ``obj[2]`` if that is valid, else ``XYZ``.

This enhancement applies to single and multiple attribute/item
retrievals, with the default value returned for any missing attribute or
item.
- **getitem**: ``getitem(obj, k, XYZ)`` or
``getitem(obj, k, default=XYZ)`` would return ``obj[k]`` if that is valid,
else ``XYZ``.

In the first two cases, the enhancement applies to single and multiple
attribute/item retrievals, with the default value returned for any
missing attribute or item.

No functionality change is incorporated if ``default`` is not used.
No functionality change is incorporated in any case if the extra
default (keyword) argument is not used.


Examples for attrgetter
Expand Down Expand Up @@ -144,7 +153,32 @@ With this PEP, using the proposed ``default`` keyword::
('bar', 'XYZ')


.. _PEP 769 About Possible Implementations:
Examples for getitem
--------------------

The current behavior is unchanged::

>>> obj = ["foo", "bar", "baz"]
>>> getitem(obj, 1)
'bar'
>>> getitem(obj, 5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range


With this PEP, using the proposed extra default, positionally or with
a keyword::

>>> getitem(obj, 1, "XYZ")
'bar'
>>> getitem(obj, 5, "XYZ")
'XYZ'
>>> getitem(obj, 1, default="XYZ")
'bar'
>>> getitem(obj, 5, default="XYZ")
'XYZ'


About Possible Implementations
------------------------------
Expand All @@ -163,37 +197,43 @@ be impossible to distinguish what it returned on each step when an
attribute chain is specified (e.g.
``attrgetter("foo.bar.baz", default=XYZ)``).

The implementation for ``itemgetter`` is not that easy. The more
straightforward way is also simple to define and
understand: attempting ``__getitem__`` and catching a possible exception
(any of the three indicated in ``__getitem__`` `reference`_). This way,
``itemgetter(123, default=XYZ)(obj)`` would be equivalent to::
The implementation for ``itemgetter`` and ``getitem`` is not that
easy. The more straightforward way is also simple to define and
understand: attempting ``__getitem__`` and catching a possible
exception (any of the three indicated in ``__getitem__`` `reference`_).
This way, ``itemgetter(123, default=XYZ)(obj)`` or
``getitem(obj, 123, default=XYZ)`` would be equivalent to::

try:
value = obj[123]
except (TypeError, IndexError, KeyError):
value = XYZ

However, this would be not as efficient as we'd want for certain cases,
e.g. using dictionaries where better performance is possible. A
more complex alternative would be::
However, for performance reasons the implementation may look more
like the following, which has the same exact behaviour::

if isinstance(obj, dict):
if type(obj) == dict:
value = obj.get(123, XYZ)
else:
try:
value = obj[123]
except (TypeError, IndexError, KeyError):
value = XYZ

While this provides better performance, it is more complicated to implement and explain. This is
the first case in the `Open Issues <PEP 769 Open Issues_>`__ section later.
Note how the verification is about the exact type and not using
``isinstance``; this is to ensure the exact behaviour, which would be
impossible if the object is a user defined one that inherits ``dict``
but overwrites ``get`` (similar reason to not check if the object has
a ``get`` method).

This way, performance is better but it's just an implementation detail,
so we can keep the original explanation on how it behaves.


Corner Cases
------------

Providing a ``default`` option would only work when accessing the
Providing a ``default`` option would only work if accessing the
item/attribute would fail in the normal case. In other words, the
object accessed should not handle defaults itself.

Expand Down Expand Up @@ -255,8 +295,8 @@ combinations of ``attrgetter`` and ``itemgetter``, e.g.::
(1, 2)

However, combining ``itemgetter`` or ``attrgetter`` is totally new
behavior and very complex to define. While not impossible, it is beyond the scope of
this PEP.
behavior and very complex to define. While not impossible, it is beyond
the scope of this PEP.

In the end, having multiple default values was deemed overly complex and
potentially confusing, and a single ``default`` parameter was favored for
Expand Down Expand Up @@ -286,49 +326,7 @@ PEP.
Open Issues
===========

Behavior Equivalence for ``itemgetter``
---------------------------------------

For ``itemgetter``, should it just attempt to
access the item and capture exceptions regardless of the object's API, or
should it first validate that the object provides a ``get`` method, and if so use it to
retrieve the item with a default? See examples in the `About Possible
Implementations <PEP 769 About Possible Implementations_>`__ subsection
above.

This would help performance for the case of dictionaries, but would make
the ``default`` feature somewhat more difficult to explain, and a little
confusing if some object that is not a dictionary but still provides a ``get``
method. Alternatively, we could call ``.get`` *only* if the
object is an instance of ``dict``.

In any case, it is desirable that we do *not* affect performance
at all if the ``default`` is not triggered. Checking for ``.get`` would
be faster for dicts, but implies doing a verification
in all cases. Using the try/except model would make it less efficient as possible
in the case of dictionaries, but only if the
default is not triggered.


Add a Default to ``getitem``
----------------------------

It was proposed that we could also enhance ``getitem``, as part of
this PEP, adding the ``default`` keyword to that function as well.

This will not only improve ``getitem`` itself, but we would also gain
internal consistency in the ``operator`` module and in comparison with
the ``getattr`` builtin function, which also has a default.

The definition could be as simple as the try/except proposed above, so
doing ``getitem(obj, name, default)`` would be equivalent to::

try:
result = obj[name]
except (TypeError, IndexError, KeyError):
result = default

(However see previous open issue about special case for dictionaries.)
There are no open issues at this time.


How to Teach This
Expand Down

0 comments on commit b46a97f

Please sign in to comment.