Split-out a fourth section in the descriptor HowTo guide (GH-22965)
This commit is contained in:
parent
492d513ccb
commit
e6a7ea4f2e
|
@ -13,7 +13,7 @@ Descriptor HowTo Guide
|
|||
:term:`Descriptors <descriptor>` let objects customize attribute lookup,
|
||||
storage, and deletion.
|
||||
|
||||
This HowTo guide has three major sections:
|
||||
This guide has four major sections:
|
||||
|
||||
1) The "primer" gives a basic overview, moving gently from simple examples,
|
||||
adding one feature at a time. It is a great place to start.
|
||||
|
@ -25,6 +25,11 @@ This HowTo guide has three major sections:
|
|||
detailed mechanics of how descriptors work. Most people don't need this
|
||||
level of detail.
|
||||
|
||||
4) The last section has pure Python equivalents for built-in descriptors that
|
||||
are written in C. Read this if you're curious about how functions turn
|
||||
into bound methods or about how to implement common tools like
|
||||
:func:`classmethod`, :func:`staticmethod`, and :func:`property`.
|
||||
|
||||
|
||||
Primer
|
||||
^^^^^^
|
||||
|
@ -99,7 +104,7 @@ different, updated answers each time::
|
|||
3
|
||||
>>> os.system('touch games/newfile') # Add a fourth file to the directory
|
||||
0
|
||||
>>> g.size
|
||||
>>> g.size # Automatically updated
|
||||
4
|
||||
>>> s.size # The songs directory has twenty files
|
||||
20
|
||||
|
@ -197,7 +202,7 @@ be recorded, giving each descriptor its own *public_name* and *private_name*::
|
|||
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class LoggedAccess:
|
||||
|
||||
|
@ -259,7 +264,7 @@ A :term:`descriptor` is what we call any object that defines :meth:`__get__`,
|
|||
: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
|
||||
used in cases where a descriptor needs to know either the class where it was
|
||||
created or the name of class variable it was assigned to.
|
||||
|
||||
Descriptors get invoked by the dot operator during attribute lookup. If a
|
||||
|
@ -318,7 +323,7 @@ managed attribute descriptor::
|
|||
def validate(self, value):
|
||||
pass
|
||||
|
||||
Custom validators need to subclass from :class:`Validator` and supply a
|
||||
Custom validators need to inherit from :class:`Validator` and must supply a
|
||||
:meth:`validate` method to test various restrictions as needed.
|
||||
|
||||
|
||||
|
@ -334,8 +339,9 @@ Here are three practical data validation utilities:
|
|||
minimum or maximum.
|
||||
|
||||
3) :class:`String` verifies that a value is a :class:`str`. Optionally, it
|
||||
validates a given minimum or maximum length. Optionally, it can test for
|
||||
another predicate as well.
|
||||
validates a given minimum or maximum length. It can validate a
|
||||
user-defined `predicate
|
||||
<https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)>`_ as well.
|
||||
|
||||
::
|
||||
|
||||
|
@ -398,7 +404,7 @@ Here's how the data validators can be used in a real class::
|
|||
class Component:
|
||||
|
||||
name = String(minsize=3, maxsize=10, predicate=str.isupper)
|
||||
kind = OneOf('plastic', 'metal')
|
||||
kind = OneOf('wood', 'metal', 'plastic')
|
||||
quantity = Number(minvalue=0)
|
||||
|
||||
def __init__(self, name, kind, quantity):
|
||||
|
@ -426,9 +432,7 @@ Abstract
|
|||
--------
|
||||
|
||||
Defines descriptors, summarizes the protocol, and shows how descriptors are
|
||||
called. Examines a custom descriptor and several built-in Python descriptors
|
||||
including functions, properties, static methods, and class methods. Shows how
|
||||
each works by giving a pure Python equivalent and a sample application.
|
||||
called. Provides an example showing how object relational mappings work.
|
||||
|
||||
Learning about descriptors not only provides access to a larger toolset, it
|
||||
creates a deeper understanding of how Python works and an appreciation for the
|
||||
|
@ -519,24 +523,17 @@ The full C implementation can be found in :c:func:`PyObject_GenericGetAttr()` in
|
|||
|
||||
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"
|
||||
v = object.__getattribute__(cls, key)
|
||||
if hasattr(v, '__get__'):
|
||||
return v.__get__(None, cls)
|
||||
return v
|
||||
The full C implementation can be found in :c:func:`type_getattro()` in
|
||||
:source:`Objects/typeobject.c`.
|
||||
|
||||
**Super**: The machinery is in the custom :meth:`__getattribute__` method for
|
||||
object returned by :class:`super()`.
|
||||
|
||||
The attribute lookup ``super(A, obj).m`` searches ``obj.__class__.__mro__`` for
|
||||
the base class ``B`` immediately following ``A`` and then returns
|
||||
``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 :meth:`object.__getattribute__`.
|
||||
``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
|
||||
:meth:`object.__getattribute__`.
|
||||
|
||||
The implementation details are in :c:func:`super_getattro()` in
|
||||
:source:`Objects/typeobject.c`. A pure Python equivalent can be found in
|
||||
|
@ -544,9 +541,9 @@ The implementation details are in :c:func:`super_getattro()` in
|
|||
|
||||
.. _`Guido's Tutorial`: https://www.python.org/download/releases/2.2.3/descrintro/#cooperation
|
||||
|
||||
**Summary**: The details listed above show that the mechanism for descriptors is
|
||||
embedded in the :meth:`__getattribute__()` methods for :class:`object`,
|
||||
:class:`type`, and :func:`super`.
|
||||
**Summary**: 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:
|
||||
|
||||
|
@ -586,15 +583,16 @@ place at the time of class creation. If descriptors are added to the class
|
|||
afterwards, :meth:`__set_name__` will need to be called manually.
|
||||
|
||||
|
||||
Descriptor Example
|
||||
------------------
|
||||
ORM Example
|
||||
-----------
|
||||
|
||||
The following code is simplified skeleton showing how data descriptors could
|
||||
be used to implement an `object relational mapping
|
||||
<https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping>`_.
|
||||
|
||||
The essential idea is that instances only hold keys to a database table. The
|
||||
actual data is stored in an external table that is being dynamically updated::
|
||||
The essential idea is that the data is stored in an external database. The
|
||||
Python instances only hold keys to the database's tables. Descriptors take
|
||||
care of lookups or updates::
|
||||
|
||||
class Field:
|
||||
|
||||
|
@ -609,8 +607,8 @@ actual data is stored in an external table that is being dynamically updated::
|
|||
conn.execute(self.store, [value, obj.key])
|
||||
conn.commit()
|
||||
|
||||
We can use the :class:`Field` to define "models" that describe the schema for
|
||||
each table in a database::
|
||||
We can use the :class:`Field` class to define "models" that describe the schema
|
||||
for each table in a database::
|
||||
|
||||
class Movie:
|
||||
table = 'Movies' # Table name
|
||||
|
@ -650,10 +648,13 @@ it can be updated::
|
|||
>>> Movie('Star Wars').director
|
||||
'J.J. Abrams'
|
||||
|
||||
Pure Python Equivalents
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
use cases are so common that they have been prepackaged into builtin tools.
|
||||
Properties, bound methods, static methods, and class methods are all based on
|
||||
the descriptor protocol.
|
||||
|
||||
|
||||
Properties
|
||||
|
@ -746,7 +747,7 @@ prepended to the other arguments. By convention, the instance is called
|
|||
Methods can be created manually with :class:`types.MethodType` which is
|
||||
roughly equivalent to::
|
||||
|
||||
class Method:
|
||||
class MethodType:
|
||||
"Emulate Py_MethodType in Objects/classobject.c"
|
||||
|
||||
def __init__(self, func, obj):
|
||||
|
@ -770,7 +771,7 @@ during dotted lookup from an instance. Here's how it works::
|
|||
"Simulate func_descr_get() in Objects/funcobject.c"
|
||||
if obj is None:
|
||||
return self
|
||||
return types.MethodType(self, obj)
|
||||
return MethodType(self, obj)
|
||||
|
||||
Running the following class in the interpreter shows how the function
|
||||
descriptor works in practice::
|
||||
|
@ -816,8 +817,8 @@ 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
|
||||
--------------
|
||||
|
||||
Non-data descriptors provide a simple mechanism for variations on the usual
|
||||
patterns of binding functions into methods.
|
||||
|
@ -883,6 +884,10 @@ Using the non-data descriptor protocol, a pure Python version of
|
|||
def __get__(self, obj, objtype=None):
|
||||
return self.f
|
||||
|
||||
|
||||
Class Methods
|
||||
-------------
|
||||
|
||||
Unlike static methods, class methods prepend the class reference to the
|
||||
argument list before calling the function. This format is the same
|
||||
for whether the caller is an object or a class::
|
||||
|
@ -897,12 +902,11 @@ for whether the caller is an object or a class::
|
|||
>>> print(F().f(3))
|
||||
('F', 3)
|
||||
|
||||
|
||||
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
|
||||
class methods is to create alternate class constructors. The classmethod
|
||||
:func:`dict.fromkeys` creates a new dictionary from a list of keys. The pure
|
||||
Python equivalent is::
|
||||
This behavior is useful whenever the method only needs to have a class
|
||||
reference and does rely on data stored in a specific instance. One use for
|
||||
class methods is to create alternate class constructors. For example, the
|
||||
classmethod :func:`dict.fromkeys` creates a new dictionary from a list of
|
||||
keys. The pure Python equivalent is::
|
||||
|
||||
class Dict:
|
||||
...
|
||||
|
@ -934,7 +938,7 @@ Using the non-data descriptor protocol, a pure Python version of
|
|||
cls = type(obj)
|
||||
if hasattr(obj, '__get__'):
|
||||
return self.f.__get__(cls)
|
||||
return types.MethodType(self.f, cls)
|
||||
return MethodType(self.f, cls)
|
||||
|
||||
The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and
|
||||
makes it possible for :func:`classmethod` to support chained decorators.
|
||||
|
|
Loading…
Reference in New Issue