Skip to content
Merged
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
96 changes: 50 additions & 46 deletions Doc/howto/descriptor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
^^^^^^
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.


Expand All @@ -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.

::

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -519,34 +523,27 @@ 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
`Guido's Tutorial`_.

.. _`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:

Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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::
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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::
Expand All @@ -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:
...
Expand Down Expand Up @@ -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.
Expand Down