Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
bpo-20751: Clarify descriptor
I fear that the section "invoking descriptor" was quite hard to grasp on first
reading. I try to improve this with the following changes.

The notion of lookup chain was used without ever being defined anywhere in the
documentation. I add a short definition.

> may override the default behavior and invoke the descriptor method instead

was changed. It seemed to indicate that descriptor replace the behavior
described previously, of checking for the value associated to `x` in `a` in the
lookup chain. To the best of my understanding, the lookup chain is still used,
since we do not even know whether `x` is a descriptor or not until the lookup
chain returned a value. It would be more exact to indicate that a descriptor
override the action of returning, assigning or deleting.

> The starting point for descriptor invocation is a binding, ``a.x``. How the
> arguments are assembled depends on ``a``:

While #29909 was a net improvement, I fear it was not sufficient. I had three
issues while reading this section for a first time.
* The Direct Call actually do not use the binding `a.x` (that's the very point
of the direct call), so the Direct Call should not have been in this
enumeration. In fact, what is even worse is that in the Direct Call, `x` is a
variable while in all other case, `x` is an attribute name.
* Furthermore, once I was reading the Super Binding, it was confusing that what
was called `a` in the introduction of the list is now called `super(A, a)`. At
this point, I believe it is more clear to just state that there are four ways to
invoke a descriptor and to list them. Each case introducing the variables it
requires.pp
* Super Binding uses the name "dotted lookup" while the remaining of the
documentation uses "attribute access". This lead me to assume that there is a
reason why a different name was used, that it was referencing a different
concept. I believe it is best to be consistent with naming of a concept.
* Super Binding "for a base class ``B`` following ``A``" was slightly confusing,
as it seems to indicates that there may exists multiple such base class `B`. I
believe that "the base class ``B``" is clearer.

For your information "dotted lookup" is also used in the howto section; I'd
suggest to change it there too for the sake of consistency. But since the usage
is consistent throughout the howto's descriptor file, this is at least less
confusing than this usage.
  • Loading branch information
Arthur-Milchior committed Dec 26, 2021
commit 7538131d3ddc785417b8d2418574548b2ac348f5
37 changes: 22 additions & 15 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1796,36 +1796,43 @@ protocol: :meth:`~object.__get__`, :meth:`~object.__set__`, and
:meth:`~object.__delete__`. If any of
those methods are defined for an object, it is said to be a descriptor.

.. index::
single: lookup chain

The default behavior for attribute access is to get, set, or delete the
attribute from an object's dictionary. For instance, ``a.x`` has a lookup chain
starting with ``a.__dict__['x']``, then ``type(a).__dict__['x']``, and
continuing through the base classes of ``type(a)`` excluding metaclasses.
attribute from an object's dictionary. For instance, ``a.x`` lookup for ``'x'``
in the dictionaries ``a.__dict__``, then ``type(a).__dict__``, and continuing
through the base classes of ``type(a)`` excluding metaclasses. This list of
dictionaries is called the lookup chain of ``a``. By default ``a.x`` returns
``d['x']`` for ``d`` the first dictionary in the lookup chain containing
``'x'``.

However, if the looked-up value is an object defining one of the descriptor
methods, then Python may override the default behavior and invoke the descriptor
method instead. Where this occurs in the precedence chain depends on which
descriptor methods were defined and how they were called.
methods, then Python may override the default behavior. Instead of directly
returning the value, assigning to it or deleting it, it invokes the descriptor
method. Where this occurs in the precedence chain depends on which descriptor
methods were defined and how they were called.

The starting point for descriptor invocation is a binding, ``a.x``. How the
arguments are assembled depends on ``a``:
There are four ways to invoke a descriptor.

Direct Call
The simplest and least common call is when user code directly invokes a
descriptor method: ``x.__get__(a)``.
descriptor method: ``x.__get__(a)`` for ``x`` a descriptor and some value
``a``.

Instance Binding
If binding to an object instance, ``a.x`` is transformed into the call:
For ``a`` an object instance, ``a.x`` is transformed into the call:
``type(a).__dict__['x'].__get__(a, type(a))``.

Class Binding
If binding to a class, ``A.x`` is transformed into the call:
For ``A`` a class, ``A.x`` is transformed into the call:
``A.__dict__['x'].__get__(None, A)``.

Super Binding
A dotted lookup such as ``super(A, a).x`` searches
``a.__class__.__mro__`` for a base class ``B`` following ``A`` and then
returns ``B.__dict__['x'].__get__(a, A)``. If not a descriptor, ``x`` is
returned unchanged.
For ``a`` an instance of ``A``, ``super(A, a).m(*args, **kwargs)`` searches
``a.__class__.__mro__`` for the first base class ``B`` following ``A`` having
a method ``m`` and then returns ``B.__dict__['m'].__get__(a, A)(*args,
**kwargs)``.

.. testcode::
:hide:
Expand Down