Second round of updates to the descriptor howto guide (GH-22946) (GH-22958)

This commit is contained in:
Miss Skeleton (bot) 2020-10-24 20:39:15 -07:00 committed by GitHub
parent 9cf26b00e4
commit 3d43f1dce3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 156 additions and 96 deletions

View File

@ -29,8 +29,8 @@ This HowTo guide has three major sections:
Primer Primer
^^^^^^ ^^^^^^
In this primer, we start with most basic possible example and then we'll add In this primer, we start with the most basic possible example and then we'll
new capabilities one by one. add new capabilities one by one.
Simple example: A descriptor that returns a constant Simple example: A descriptor that returns a constant
@ -197,7 +197,7 @@ be recorded, giving each descriptor its own *public_name* and *private_name*::
import logging import logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO, force=True)
class LoggedAccess: class LoggedAccess:
@ -258,6 +258,10 @@ Closing thoughts
A :term:`descriptor` is what we call any object that defines :meth:`__get__`, A :term:`descriptor` is what we call any object that defines :meth:`__get__`,
:meth:`__set__`, or :meth:`__delete__`. :meth:`__set__`, or :meth:`__delete__`.
Optionally, descriptors can have a :meth:`__set_name__` method. This is only
used in cases where a descriptor needs to know either the class where it is
created or the name of class variable it was assigned to.
Descriptors get invoked by the dot operator during attribute lookup. If a Descriptors get invoked by the dot operator during attribute lookup. If a
descriptor is accessed indirectly with ``vars(some_class)[descriptor_name]``, descriptor is accessed indirectly with ``vars(some_class)[descriptor_name]``,
the descriptor instance is returned without invoking it. the descriptor instance is returned without invoking it.
@ -291,7 +295,7 @@ Validator class
A validator is a descriptor for managed attribute access. Prior to storing A validator is a descriptor for managed attribute access. Prior to storing
any data, it verifies that the new value meets various type and range any data, it verifies that the new value meets various type and range
restrictions. If those restrictions aren't met, it raises an exception to restrictions. If those restrictions aren't met, it raises an exception to
prevents data corruption at its source. prevent data corruption at its source.
This :class:`Validator` class is both an :term:`abstract base class` and a This :class:`Validator` class is both an :term:`abstract base class` and a
managed attribute descriptor:: managed attribute descriptor::
@ -438,12 +442,12 @@ In general, a descriptor is an object attribute with "binding behavior", one
whose attribute access has been overridden by methods in the descriptor whose attribute access has been overridden by methods in the descriptor
protocol. Those methods are :meth:`__get__`, :meth:`__set__`, and protocol. Those methods are :meth:`__get__`, :meth:`__set__`, and
:meth:`__delete__`. If any of those methods are defined for an object, it is :meth:`__delete__`. If any of those methods are defined for an object, it is
said to be a descriptor. said to be a :term:`descriptor`.
The default behavior for attribute access is to get, set, or delete the 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 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 starting with ``a.__dict__['x']``, then ``type(a).__dict__['x']``, and
continuing through the base classes of ``type(a)`` excluding metaclasses. If the continuing through the base classes of ``type(a)``. If the
looked-up value is an object defining one of the descriptor methods, then Python 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. may override the default behavior and invoke the descriptor method instead.
Where this occurs in the precedence chain depends on which descriptor methods Where this occurs in the precedence chain depends on which descriptor methods
@ -492,60 +496,76 @@ Invoking Descriptors
A descriptor can be called directly by its method name. For example, A descriptor can be called directly by its method name. For example,
``d.__get__(obj)``. ``d.__get__(obj)``.
Alternatively, it is more common for a descriptor to be invoked automatically But it is more common for a descriptor to be invoked automatically from
upon attribute access. For example, ``obj.d`` looks up ``d`` in the dictionary attribute access. The expression ``obj.d`` looks up ``d`` in the dictionary of
of ``obj``. If ``d`` defines the method :meth:`__get__`, then ``d.__get__(obj)`` ``obj``. If ``d`` defines the method :meth:`__get__`, then ``d.__get__(obj)``
is invoked according to the precedence rules listed below. is invoked according to the precedence rules listed below.
The details of invocation depend on whether ``obj`` is an object or a class. The details of invocation depend on whether ``obj`` is an object, class, or
instance of super.
For objects, the machinery is in :meth:`object.__getattribute__` which **Objects**: The machinery is in :meth:`object.__getattribute__`.
transforms ``b.x`` into ``type(b).__dict__['x'].__get__(b, type(b))``. The
implementation works through a precedence chain that gives data descriptors It transforms ``b.x`` into ``type(b).__dict__['x'].__get__(b, type(b))``.
The implementation works through a precedence chain that gives data descriptors
priority over instance variables, instance variables priority over non-data priority over instance variables, instance variables priority over non-data
descriptors, and assigns lowest priority to :meth:`__getattr__` if provided. descriptors, and assigns lowest priority to :meth:`__getattr__` if provided.
The full C implementation can be found in :c:func:`PyObject_GenericGetAttr()` in The full C implementation can be found in :c:func:`PyObject_GenericGetAttr()` in
:source:`Objects/object.c`. :source:`Objects/object.c`.
For classes, the machinery is in :meth:`type.__getattribute__` which transforms **Classes**: The machinery is in :meth:`type.__getattribute__`.
``B.x`` into ``B.__dict__['x'].__get__(None, B)``. In pure Python, it looks
like::
def __getattribute__(self, key): It transforms ``A.x`` into ``A.__dict__['x'].__get__(None, A)``.
In pure Python, it looks like this::
def __getattribute__(cls, key):
"Emulate type_getattro() in Objects/typeobject.c" "Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key) v = object.__getattribute__(cls, key)
if hasattr(v, '__get__'): if hasattr(v, '__get__'):
return v.__get__(None, self) return v.__get__(None, cls)
return v return v
The important points to remember are: **Super**: The machinery is in the custom :meth:`__getattribute__` method for
object returned by :class:`super()`.
* descriptors are invoked by the :meth:`__getattribute__` method The attribute lookup ``super(A, obj).m`` searches ``obj.__class__.__mro__`` for
* overriding :meth:`__getattribute__` prevents automatic descriptor calls the base class ``B`` immediately following ``A`` and then returns
* :meth:`object.__getattribute__` and :meth:`type.__getattribute__` make ``B.__dict__['m'].__get__(obj, A)``.
different calls to :meth:`__get__`.
* data descriptors always override instance dictionaries.
* non-data descriptors may be overridden by instance dictionaries.
The object returned by ``super()`` also has a custom :meth:`__getattribute__` If not a descriptor, ``m`` is returned unchanged. If not in the dictionary,
method for invoking descriptors. The attribute lookup ``super(B, obj).m`` searches ``m`` reverts to a search using :meth:`object.__getattribute__`.
``obj.__class__.__mro__`` for the base class ``A`` immediately following ``B``
and then returns ``A.__dict__['m'].__get__(obj, B)``. If not a descriptor,
``m`` is returned unchanged. If not in the dictionary, ``m`` reverts to a
search using :meth:`object.__getattribute__`.
The implementation details are in :c:func:`super_getattro()` in The implementation details are in :c:func:`super_getattro()` in
:source:`Objects/typeobject.c`. and 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`_.
.. _`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
The details above show that the mechanism for descriptors is embedded in the **Summary**: The details listed above show that the mechanism for descriptors is
:meth:`__getattribute__()` methods for :class:`object`, :class:`type`, and embedded in the :meth:`__getattribute__()` methods for :class:`object`,
:func:`super`. Classes inherit this machinery when they derive from :class:`type`, and :func:`super`.
:class:`object` or if they have a metaclass providing similar functionality.
Likewise, classes can turn-off descriptor invocation by overriding The important points to remember are:
:meth:`__getattribute__()`.
* Descriptors are invoked by the :meth:`__getattribute__` method.
* Classes inherit this machinery from :class:`object`, :class:`type`, or
:func:`super`.
* Overriding :meth:`__getattribute__` prevents automatic descriptor calls
because all the descriptor logic is in that method.
* :meth:`object.__getattribute__` and :meth:`type.__getattribute__` make
different calls to :meth:`__get__`. The first includes the instance and may
include the class. The second puts in ``None`` for the instance and always
includes the class.
* Data descriptors always override instance dictionaries.
* Non-data descriptors may be overridden by instance dictionaries.
Automatic Name Notification Automatic Name Notification
@ -569,47 +589,70 @@ afterwards, :meth:`__set_name__` will need to be called manually.
Descriptor Example Descriptor Example
------------------ ------------------
The following code creates a class whose objects are data descriptors which The following code is simplified skeleton showing how data descriptors could
print a message for each get or set. Overriding :meth:`__getattribute__` is be used to implement an `object relational mapping
alternate approach that could do this for every attribute. However, this <https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping>`_.
descriptor is useful for monitoring just a few chosen attributes::
class RevealAccess: The essential idea is that instances only hold keys to a database table. The
"""A data descriptor that sets and returns values actual data is stored in an external table that is being dynamically updated::
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'): class Field:
self.val = initval
self.name = name def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
print('Retrieving', self.name) return conn.execute(self.fetch, [obj.key]).fetchone()[0]
return self.val
def __set__(self, obj, val): def __set__(self, obj, value):
print('Updating', self.name) conn.execute(self.store, [value, obj.key])
self.val = val conn.commit()
class B: We can use the :class:`Field` to define "models" that describe the schema for
x = RevealAccess(10, 'var "x"') each table in a database::
y = 5
>>> m = B() class Movie:
>>> m.x table = 'Movies' # Table name
Retrieving var "x" key = 'title' # Primary key
10 director = Field()
>>> m.x = 20 year = Field()
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
The protocol is simple and offers exciting possibilities. Several use cases are def __init__(self, key):
so common that they have been packaged into individual function calls. self.key = key
Properties, bound methods, static methods, and class methods are all
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
def __init__(self, key):
self.key = key
An interactive session shows how data is retrieved from the database and how
it can be updated::
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'
>>> Song('Country Roads').artist
'John Denver'
>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'
The descriptor protocol is simple and offers exciting possibilities. Several
use cases are so common that they have been packaged into individual function
calls. Properties, bound methods, static methods, and class methods are all
based on the descriptor protocol. based on the descriptor protocol.
@ -619,7 +662,7 @@ Properties
Calling :func:`property` is a succinct way of building a data descriptor that Calling :func:`property` is a succinct way of building a data descriptor that
triggers function calls upon access to an attribute. Its signature is:: triggers function calls upon access to an attribute. Its signature is::
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute property(fget=None, fset=None, fdel=None, doc=None) -> property
The documentation shows a typical use to define a managed attribute ``x``:: The documentation shows a typical use to define a managed attribute ``x``::
@ -695,17 +738,30 @@ Functions and Methods
Python's object oriented features are built upon a function based environment. Python's object oriented features are built upon a function based environment.
Using non-data descriptors, the two are merged seamlessly. Using non-data descriptors, the two are merged seamlessly.
Class dictionaries store methods as functions. In a class definition, methods Functions stored in class dictionaries get turned into methods when invoked.
are written using :keyword:`def` or :keyword:`lambda`, the usual tools for Methods only differ from regular functions in that the object instance is
creating functions. Methods only differ from regular functions in that the prepended to the other arguments. By convention, the instance is called
first argument is reserved for the object instance. By Python convention, the *self* but could be called *this* or any other variable name.
instance reference is called *self* but may be called *this* or any other
variable name.
To support method calls, functions include the :meth:`__get__` method for Methods can be created manually with :class:`types.MethodType` which is
binding methods during attribute access. This means that all functions are roughly equivalent to::
non-data descriptors which return bound methods when they are invoked from an
object. In pure Python, it works like this:: class Method:
"Emulate Py_MethodType in Objects/classobject.c"
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
return func(obj, *args, **kwargs)
To support automatic creation of methods, functions include the
:meth:`__get__` method for binding methods during attribute access. This
means that functions are non-data descriptors which return bound methods
during dotted lookup from an instance. Here's how it works::
class Function: class Function:
... ...
@ -716,15 +772,20 @@ object. In pure Python, it works like this::
return self return self
return types.MethodType(self, obj) return types.MethodType(self, obj)
Running the following in class in the interpreter shows how the function Running the following class in the interpreter shows how the function
descriptor works in practice:: descriptor works in practice::
class D: class D:
def f(self, x): def f(self, x):
return x return x
Access through the class dictionary does not invoke :meth:`__get__`. Instead, The function has a :term:`qualified name` attribute to support introspection::
it just returns the underlying function object::
>>> D.f.__qualname__
'D.f'
Accessing the function through the class dictionary does not invoke
:meth:`__get__`. Instead, it just returns the underlying function object::
>>> D.__dict__['f'] >>> D.__dict__['f']
<function D.f at 0x00C45070> <function D.f at 0x00C45070>
@ -735,13 +796,8 @@ underlying function unchanged::
>>> D.f >>> D.f
<function D.f at 0x00C45070> <function D.f at 0x00C45070>
The function has a :term:`qualified name` attribute to support introspection:: The interesting behavior occurs during dotted access from an instance. The
dotted lookup calls :meth:`__get__` which returns a bound method object::
>>> D.f.__qualname__
'D.f'
Dotted access from an instance calls :meth:`__get__` which returns a bound
method object::
>>> d = D() >>> d = D()
>>> d.f >>> d.f
@ -752,9 +808,13 @@ instance::
>>> d.f.__func__ >>> d.f.__func__
<function D.f at 0x1012e5ae8> <function D.f at 0x1012e5ae8>
>>> d.f.__self__ >>> d.f.__self__
<__main__.D object at 0x1012e1f98> <__main__.D object at 0x1012e1f98>
If you have ever wondered where *self* comes from in regular methods or where
*cls* comes from in class methods, this is it!
Static Methods and Class Methods Static Methods and Class Methods
-------------------------------- --------------------------------
@ -798,8 +858,8 @@ in statistical work but does not directly depend on a particular dataset.
It can be called either from an object or the class: ``s.erf(1.5) --> .9332`` or It can be called either from an object or the class: ``s.erf(1.5) --> .9332`` or
``Sample.erf(1.5) --> .9332``. ``Sample.erf(1.5) --> .9332``.
Since staticmethods return the underlying function with no changes, the example Since static methods return the underlying function with no changes, the
calls are unexciting:: example calls are unexciting::
class E: class E:
@staticmethod @staticmethod
@ -840,7 +900,7 @@ for whether the caller is an object or a class::
This behavior is useful whenever the function only needs to have a class This behavior is useful whenever the function only needs to have a class
reference and does not care about any underlying data. One use for reference and does not care about any underlying data. One use for
classmethods is to create alternate class constructors. The classmethod class methods is to create alternate class constructors. The classmethod
:func:`dict.fromkeys` creates a new dictionary from a list of keys. The pure :func:`dict.fromkeys` creates a new dictionary from a list of keys. The pure
Python equivalent is:: Python equivalent is::