Expand and clarify the "Invoking Descriptors" section of the Descriptor HowTo (GH-23078)

This commit is contained in:
Raymond Hettinger 2020-11-01 09:10:06 -08:00 committed by GitHub
parent 7feb54a634
commit 148c76b27c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 79 additions and 38 deletions

View File

@ -52,7 +52,7 @@ To use the descriptor, it must be stored as a class variable in another class::
class A: class A:
x = 5 # Regular class attribute x = 5 # Regular class attribute
y = Ten() # Descriptor y = Ten() # Descriptor instance
An interactive session shows the difference between normal attribute lookup An interactive session shows the difference between normal attribute lookup
and descriptor lookup:: and descriptor lookup::
@ -80,7 +80,6 @@ Dynamic lookups
Interesting descriptors typically run computations instead of doing lookups:: Interesting descriptors typically run computations instead of doing lookups::
import os import os
class DirectorySize: class DirectorySize:
@ -90,7 +89,7 @@ Interesting descriptors typically run computations instead of doing lookups::
class Directory: class Directory:
size = DirectorySize() # Descriptor size = DirectorySize() # Descriptor instance
def __init__(self, dirname): def __init__(self, dirname):
self.dirname = dirname # Regular instance attribute self.dirname = dirname # Regular instance attribute
@ -147,11 +146,11 @@ the lookup or update::
class Person: class Person:
age = LoggedAgeAccess() # Descriptor age = LoggedAgeAccess() # Descriptor instance
def __init__(self, name, age): def __init__(self, name, age):
self.name = name # Regular instance attribute self.name = name # Regular instance attribute
self.age = age # Calls the descriptor self.age = age # Calls __set__()
def birthday(self): def birthday(self):
self.age += 1 # Calls both __get__() and __set__() self.age += 1 # Calls both __get__() and __set__()
@ -221,8 +220,8 @@ be recorded, giving each descriptor its own *public_name* and *private_name*::
class Person: class Person:
name = LoggedAccess() # First descriptor name = LoggedAccess() # First descriptor instance
age = LoggedAccess() # Second descriptor age = LoggedAccess() # Second descriptor instance
def __init__(self, name, age): def __init__(self, name, age):
self.name = name # Calls the first descriptor self.name = name # Calls the first descriptor
@ -494,56 +493,98 @@ called. Defining the :meth:`__set__` method with an exception raising
placeholder is enough to make it a data descriptor. placeholder is enough to make it a data descriptor.
Invoking Descriptors Overview of Descriptor Invocation
-------------------- ---------------------------------
A descriptor can be called directly by its method name. For example, A descriptor can be called directly with ``desc.__get__(obj)`` or
``d.__get__(obj)``. ``desc.__get__(None, cls)``.
But it is more common for a descriptor to be invoked automatically from But it is more common for a descriptor to be invoked automatically from
attribute access. The expression ``obj.d`` looks up ``d`` in the dictionary of attribute access.
``obj``. If ``d`` defines the method :meth:`__get__`, then ``d.__get__(obj)``
is invoked according to the precedence rules listed below. The expression ``obj.x`` looks up the attribute ``x`` in the chain of
namespaces for ``obj``. If the search finds a descriptor, its :meth:`__get__`
method is invoked according to the precedence rules listed below.
The details of invocation depend on whether ``obj`` is an object, class, or The details of invocation depend on whether ``obj`` is an object, class, or
instance of super. instance of super.
**Objects**: The machinery is in :meth:`object.__getattribute__`.
It transforms ``b.x`` into ``type(b).__dict__['x'].__get__(b, type(b))``. Invocation from an Instance
---------------------------
The implementation works through a precedence chain that gives data descriptors Instance lookup scans through a chain of namespaces giving data descriptors
priority over instance variables, instance variables priority over non-data the highest priority, followed by instance variables, then non-data
descriptors, and assigns lowest priority to :meth:`__getattr__` if provided. descriptors, then class variables, and lastly :meth:`__getattr__` if it is
provided.
The full C implementation can be found in :c:func:`PyObject_GenericGetAttr()` in If a descriptor is found for ``a.x``, then it is invoked with:
:source:`Objects/object.c`. ``desc.__get__(a, type(a))``.
**Classes**: The machinery is in :meth:`type.__getattribute__`. The logic for a dotted lookup is in :meth:`object.__getattribute__`. Here is
a pure Python equivalent::
It transforms ``A.x`` into ``A.__dict__['x'].__get__(None, A)``. def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
null = object()
objtype = type(obj)
value = getattr(objtype, name, null)
if value is not null and hasattr(value, '__get__'):
if hasattr(value, '__set__') or hasattr(value, '__delete__'):
return value.__get__(obj, objtype) # data descriptor
try:
return vars(obj)[name] # instance variable
except (KeyError, TypeError):
pass
if hasattr(value, '__get__'):
return value.__get__(obj, objtype) # non-data descriptor
if value is not null:
return value # class variable
# Emulate slot_tp_getattr_hook() in Objects/typeobject.c
if hasattr(objtype, '__getattr__'):
return objtype.__getattr__(obj, name) # __getattr__ hook
raise AttributeError(name)
The full C implementation can be found in :c:func:`type_getattro()` in The :exc:`TypeError` exception handler is needed because the instance dictionary
:source:`Objects/typeobject.c`. doesn't exist when its class defines :term:`__slots__`.
**Super**: The machinery is in the custom :meth:`__getattribute__` method for
Invocation from a Class
-----------------------
The logic for a dotted lookup such as ``A.x`` is in
:meth:`type.__getattribute__`. The steps are similar to those for
:meth:`object.__getattribute__` but the instance dictionary lookup is replaced
by a search through the class's :term:`method resolution order`.
If a descriptor is found, it is invoked with ``desc.__get__(None, A)``.
The full C implementation can be found in :c:func:`type_getattro()` and
:c:func:`_PyType_Lookup()` in :source:`Objects/typeobject.c`.
Invocation from Super
---------------------
The logic for super's dotted lookup is in the :meth:`__getattribute__` method for
object returned by :class:`super()`. object returned by :class:`super()`.
The attribute lookup ``super(A, obj).m`` searches ``obj.__class__.__mro__`` for A dotted lookup such as ``super(A, obj).m`` searches ``obj.__class__.__mro__``
the base class ``B`` immediately following ``A`` and then returns for the base class ``B`` immediately following ``A`` and then returns
``B.__dict__['m'].__get__(obj, A)``. If not a descriptor, ``m`` is returned ``B.__dict__['m'].__get__(obj, A)``. If not a descriptor, ``m`` is returned
unchanged. If not in the dictionary, ``m`` reverts to a search using unchanged.
:meth:`object.__getattribute__`.
The implementation details are in :c:func:`super_getattro()` in The full C implementation can be found in :c:func:`super_getattro()` in
:source:`Objects/typeobject.c`. A pure Python equivalent can be found in :source:`Objects/typeobject.c`. A pure Python equivalent can be found in
`Guido's Tutorial`_. `Guido's Tutorial
<https://www.python.org/download/releases/2.2.3/descrintro/#cooperation>`_.
.. _`Guido's Tutorial`: https://www.python.org/download/releases/2.2.3/descrintro/#cooperation
**Summary**: The mechanism for descriptors is embedded in the Summary of Invocation Logic
:meth:`__getattribute__()` methods for :class:`object`, :class:`type`, and ---------------------------
:func:`super`.
The mechanism for descriptors is embedded in the :meth:`__getattribute__()`
methods for :class:`object`, :class:`type`, and :func:`super`.
The important points to remember are: The important points to remember are:
@ -652,7 +693,7 @@ Pure Python Equivalents
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
The descriptor protocol is simple and offers exciting possibilities. Several The descriptor protocol is simple and offers exciting possibilities. Several
use cases are so common that they have been prepackaged into builtin tools. use cases are so common that they have been prepackaged into built-in tools.
Properties, bound methods, static methods, and class methods are all based on Properties, bound methods, static methods, and class methods are all based on
the descriptor protocol. the descriptor protocol.

View File

@ -5,7 +5,7 @@ c-api/sequence,,:i2,o[i1:i2]
c-api/tuple,,:high,p[low:high] c-api/tuple,,:high,p[low:high]
c-api/unicode,,:end,str[start:end] c-api/unicode,,:end,str[start:end]
c-api/unicode,,:start,unicode[start:start+length] c-api/unicode,,:start,unicode[start:start+length]
distutils/examples,267,`,This is the description of the ``foobar`` package. distutils/examples,,`,This is the description of the ``foobar`` package.
distutils/setupscript,,::, distutils/setupscript,,::,
extending/embedding,,:numargs,"if(!PyArg_ParseTuple(args, "":numargs""))" extending/embedding,,:numargs,"if(!PyArg_ParseTuple(args, "":numargs""))"
extending/extending,,:myfunction,"PyArg_ParseTuple(args, ""D:myfunction"", &c);" extending/extending,,:myfunction,"PyArg_ParseTuple(args, ""D:myfunction"", &c);"

1 c-api/arg :ref PyArg_ParseTuple(args, "O|O:ref", &object, &callback)
5 c-api/tuple :high p[low:high]
6 c-api/unicode :end str[start:end]
7 c-api/unicode :start unicode[start:start+length]
8 distutils/examples 267 ` This is the description of the ``foobar`` package.
9 distutils/setupscript ::
10 extending/embedding :numargs if(!PyArg_ParseTuple(args, ":numargs"))
11 extending/extending :myfunction PyArg_ParseTuple(args, "D:myfunction", &c);