Skip to content

Commit

Permalink
PEP 769: Fix some typos and provide some clarity (#4192)
Browse files Browse the repository at this point in the history
* Fix some typos and provide some clarity

* Update peps/pep-0769.rst

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

---------

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 9, 2025
1 parent 25c9edb commit 83149c4
Showing 1 changed file with 59 additions and 58 deletions.
117 changes: 59 additions & 58 deletions peps/pep-0769.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Python-Version: 3.14
Abstract
========

This proposal aims to enhance the ``operator`` module by adding a
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
specified default value when the targeted attribute or item is missing,
Expand Down Expand Up @@ -48,7 +48,7 @@ value for all cases (more about this below in `Rejected Ideas
Specification
=============

Proposed behaviours:
Proposed behaviors:

- **attrgetter**: ``f = attrgetter("name", default=XYZ)`` followed by
``f(obj)`` would return ``obj.name`` if the attribute exists, else
Expand All @@ -67,7 +67,7 @@ No functionality change is incorporated if ``default`` is not used.
Examples for attrgetter
-----------------------

Current behaviour, no changes introduced::
The current behavior is unchanged::

>>> class C:
... class D:
Expand Down Expand Up @@ -95,7 +95,7 @@ Current behaviour, no changes introduced::
File "<stdin>", line 1, in <module>
AttributeError: type object 'D' has no attribute 'badname'

Using ``default``::
With this PEP, using the proposed ``default`` keyword::

>>> attrgetter("D", default="noclass")(C)
<class '__main__.C.D'>
Expand All @@ -114,7 +114,7 @@ Using ``default``::
Examples for itemgetter
-----------------------

Current behaviour, no changes introduced::
The current behavior is unchanged::

>>> obj = ["foo", "bar", "baz"]
>>> itemgetter(1)(obj)
Expand All @@ -131,7 +131,7 @@ Current behaviour, no changes introduced::
IndexError: list index out of range


Using ``default``::
With this PEP, using the proposed ``default`` keyword::

>>> itemgetter(1, default="XYZ")(obj)
'bar'
Expand All @@ -148,33 +148,33 @@ Using ``default``::
About Possible Implementations
------------------------------

For the case of ``attrgetter`` is quite direct: it implies using
``getattr`` catching a possible ``AttributeError``. So
The implementation of ``attrgetter`` is quite direct: it implies using
``getattr`` and catching a possible ``AttributeError``. So
``attrgetter("name", default=XYZ)(obj)`` would be like::

try:
value = getattr(obj, "name")
except (TypeError, IndexError, KeyError):
value = XYZ

Note we cannot rely on using ``gettattr`` with a default value, as would
Note we cannot rely on using ``getattr`` with a default value, as it would
be impossible to distinguish what it returned on each step when an
attribute chain is specified (e.g.
``attrgetter("foo.bar.baz", default=XYZ)``).

For the case of ``itemgetter`` it's not that easy. The more
straightforward way is similar to above, also simple to define and
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,
(any of the three indicated in ``__getitem__`` `reference`_). This way,
``itemgetter(123, default=XYZ)(obj)`` 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 particular cases,
e.g. using dictionaries where particularly good performance is desired. A
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::

if isinstance(obj, dict):
Expand All @@ -185,16 +185,16 @@ more complex alternative would be::
except (TypeError, IndexError, KeyError):
value = XYZ

Better performance, more complicated to implement and explain. This is
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.


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

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

For example, the following would be redundant/confusing because
``defaultdict`` will never error out when accessing the item::
Expand All @@ -205,8 +205,8 @@ For example, the following would be redundant/confusing because
>>> itemgetter("foo", default=-1)(dd)
0

The same applies to any user built object that overloads ``__getitem__``
or ``__getattr__`` implementing fallbacks.
The same applies to any user defined object that overloads ``__getitem__``
or ``__getattr__`` implementing its own fallbacks.


.. _PEP 769 Rejected Ideas:
Expand All @@ -221,30 +221,29 @@ The idea of allowing multiple default values for multiple attributes or
items was considered.

Two alternatives were discussed, using an iterable that must have the
same quantity of items than parameters given to
same quantity of items as parameters given to
``attrgetter``/``itemgetter``, or using a dictionary with keys matching
those names passed to ``attrgetter``/``itemgetter``.

The really complex thing to solve in these casse, that would make the
feature hard to explain and with confusing corners, is what would happen
if an iterable or dictionary is the *unique* default desired for all
The really complex thing to solve here (making the
feature hard to explain and with confusing corner cases), is what would happen
if an iterable or dictionary is the *actual* default desired for all
items. For example::

>>> itemgetter("a", default=(1, 2)({})
>>> itemgetter("a", default=(1, 2))({})
(1, 2)
>>> itemgetter("a", "b", default=(1, 2))({})
((1, 2), (1, 2))

If we allow "multiple default values" using ``default``, the first case
in the example above would raise an exception because more items in the
default than names, and the second case would return ``(1, 2))``. This is
why emerged the possibility of using a different name for multiple
defaults (``defaults``, which is expressive but maybe error prone because
too similar to ``default``).
in the example above would raise an exception because there are more items
than names in the default, and the second case would return ``(1, 2))``. This is
why we considered the possibility of using a different name for multiple
defaults (e.g. ``defaults``, which is expressive but maybe error prone because
it is too similar to ``default``).

As part of this conversation there was another proposal that would enable
multiple defaults, which is allowing combinations of ``attrgetter`` and
``itemgetter``, e.g.::
Another proposal that would enable multiple defaults, is allowing
combinations of ``attrgetter`` and ``itemgetter``, e.g.::

>>> ig_a = itemgetter("a", default=1)
>>> ig_b = itemgetter("b", default=2)
Expand All @@ -254,20 +253,20 @@ multiple defaults, which is allowing combinations of ``attrgetter`` and
>>> ig_combined({})
(1, 2)

However, combining ``itemgetter`` or ``attrgetter`` is a totally new
behaviour very complex to define, not impossible, but beyond the scope of
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.

At the end having multiple default values was deemed overly complex and
In the end, having multiple default values was deemed overly complex and
potentially confusing, and a single ``default`` parameter was favored for
simplicity and predictability.


Tuple Return Consistency
------------------------

Another rejected proposal was adding a flag to always return tuple
regardless of how many keys/names/indices were sourced to arguments.
Another rejected proposal was adding a flag to always return a tuple
regardless of how many keys/names/indices were given.
E.g.::

>>> letters = ["a", "b", "c"]
Expand All @@ -276,8 +275,8 @@ E.g.::
>>> itemgetter(1, 2, return_tuple=True)(letters)
('b', 'c')

This would be of a little help for multiple default values consistency,
but requires further discussion and for sure is out of the scope of this
This would be of little help for multiple default values consistency,
requiring further discussion, and is out of the scope of this
PEP.


Expand All @@ -286,39 +285,39 @@ PEP.
Open Issues
===========

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

We need to define how ``itemgetter`` would behave, if just attempt to
access the item and capture exceptions no matter which the object, or
validate first if the object provides a ``get`` method and use it to
retrieve the item with a default. See examples in the `About Possible
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 provides a ``get``
method is used. Alternatively, we could call ``.get`` *only* if the
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, a desirable situation is that we do *not* affect performance
In any case, it is desirable that we do *not* affect performance
at all if the ``default`` is not triggered. Checking for ``.get`` would
get the default faster in case of dicts, but implies doing a verification
in all cases. Using the try/except model would make it not as fast as it
could in the case of dictionaries, but would not introduce delays if the
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 the of
this PEP, adding ``default`` also to it.
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 that also has a default.
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::
Expand All @@ -328,15 +327,15 @@ doing ``getitem(obj, name, default)`` would be equivalent to::
except (TypeError, IndexError, KeyError):
result = default

(However see previous open issue about special case for dictionaries)
(However see previous open issue about special case for dictionaries.)


How to Teach This
=================

As the basic behaviour is not modified, this new ``default`` can be
As the basic behavior is not modified, this new ``default`` can be
avoided when teaching ``attrgetter`` and ``itemgetter`` for the first
time, and can be introduced only when the functionality need arises.
time. It can be introduced only when the functionality is needed.


Backwards Compatibility
Expand All @@ -355,6 +354,8 @@ Security Implications
Introducing a ``default`` parameter does not inherently introduce
security vulnerabilities.

.. _reference: https://docs.python.org/3/reference/datamodel.html#object.__getitem__
.. _module: https://docs.python.org/3/library/operator.html

Copyright
=========
Expand Down

0 comments on commit 83149c4

Please sign in to comment.