mirror of
https://github.com/python/cpython.git
synced 2025-07-24 19:54:21 +00:00
Split-out a fourth section in the descriptor HowTo guide (GH-22965)
This commit is contained in:
parent
492d513ccb
commit
e6a7ea4f2e
1 changed files with 50 additions and 46 deletions
|
@ -13,7 +13,7 @@ Descriptor HowTo Guide
|
||||||
:term:`Descriptors <descriptor>` let objects customize attribute lookup,
|
:term:`Descriptors <descriptor>` let objects customize attribute lookup,
|
||||||
storage, and deletion.
|
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,
|
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.
|
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
|
detailed mechanics of how descriptors work. Most people don't need this
|
||||||
level of detail.
|
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
|
Primer
|
||||||
^^^^^^
|
^^^^^^
|
||||||
|
@ -99,7 +104,7 @@ different, updated answers each time::
|
||||||
3
|
3
|
||||||
>>> os.system('touch games/newfile') # Add a fourth file to the directory
|
>>> os.system('touch games/newfile') # Add a fourth file to the directory
|
||||||
0
|
0
|
||||||
>>> g.size
|
>>> g.size # Automatically updated
|
||||||
4
|
4
|
||||||
>>> s.size # The songs directory has twenty files
|
>>> s.size # The songs directory has twenty files
|
||||||
20
|
20
|
||||||
|
@ -197,7 +202,7 @@ be recorded, giving each descriptor its own *public_name* and *private_name*::
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, force=True)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
class LoggedAccess:
|
class LoggedAccess:
|
||||||
|
|
||||||
|
@ -259,7 +264,7 @@ 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
|
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.
|
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
|
||||||
|
@ -318,7 +323,7 @@ managed attribute descriptor::
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
pass
|
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.
|
:meth:`validate` method to test various restrictions as needed.
|
||||||
|
|
||||||
|
|
||||||
|
@ -334,8 +339,9 @@ Here are three practical data validation utilities:
|
||||||
minimum or maximum.
|
minimum or maximum.
|
||||||
|
|
||||||
3) :class:`String` verifies that a value is a :class:`str`. Optionally, it
|
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
|
validates a given minimum or maximum length. It can validate a
|
||||||
another predicate as well.
|
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:
|
class Component:
|
||||||
|
|
||||||
name = String(minsize=3, maxsize=10, predicate=str.isupper)
|
name = String(minsize=3, maxsize=10, predicate=str.isupper)
|
||||||
kind = OneOf('plastic', 'metal')
|
kind = OneOf('wood', 'metal', 'plastic')
|
||||||
quantity = Number(minvalue=0)
|
quantity = Number(minvalue=0)
|
||||||
|
|
||||||
def __init__(self, name, kind, quantity):
|
def __init__(self, name, kind, quantity):
|
||||||
|
@ -426,9 +432,7 @@ Abstract
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Defines descriptors, summarizes the protocol, and shows how descriptors are
|
Defines descriptors, summarizes the protocol, and shows how descriptors are
|
||||||
called. Examines a custom descriptor and several built-in Python descriptors
|
called. Provides an example showing how object relational mappings work.
|
||||||
including functions, properties, static methods, and class methods. Shows how
|
|
||||||
each works by giving a pure Python equivalent and a sample application.
|
|
||||||
|
|
||||||
Learning about descriptors not only provides access to a larger toolset, it
|
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
|
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)``.
|
It transforms ``A.x`` into ``A.__dict__['x'].__get__(None, A)``.
|
||||||
|
|
||||||
In pure Python, it looks like this::
|
The full C implementation can be found in :c:func:`type_getattro()` in
|
||||||
|
:source:`Objects/typeobject.c`.
|
||||||
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
|
|
||||||
|
|
||||||
**Super**: The machinery is in the custom :meth:`__getattribute__` method for
|
**Super**: The machinery is in the custom :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
|
The attribute lookup ``super(A, obj).m`` searches ``obj.__class__.__mro__`` for
|
||||||
the base class ``B`` immediately following ``A`` and then returns
|
the base class ``B`` immediately following ``A`` and then returns
|
||||||
``B.__dict__['m'].__get__(obj, A)``.
|
``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
|
||||||
If not a descriptor, ``m`` is returned unchanged. If not in the dictionary,
|
:meth:`object.__getattribute__`.
|
||||||
``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`. A pure Python equivalent can be found 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
|
.. _`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
|
**Summary**: The mechanism for descriptors is embedded in the
|
||||||
embedded in the :meth:`__getattribute__()` methods for :class:`object`,
|
:meth:`__getattribute__()` methods for :class:`object`, :class:`type`, and
|
||||||
:class:`type`, and :func:`super`.
|
:func:`super`.
|
||||||
|
|
||||||
The important points to remember are:
|
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.
|
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
|
The following code is simplified skeleton showing how data descriptors could
|
||||||
be used to implement an `object relational mapping
|
be used to implement an `object relational mapping
|
||||||
<https://en.wikipedia.org/wiki/Object%E2%80%93relational_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
|
The essential idea is that the data is stored in an external database. The
|
||||||
actual data is stored in an external table that is being dynamically updated::
|
Python instances only hold keys to the database's tables. Descriptors take
|
||||||
|
care of lookups or updates::
|
||||||
|
|
||||||
class Field:
|
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.execute(self.store, [value, obj.key])
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
We can use the :class:`Field` to define "models" that describe the schema for
|
We can use the :class:`Field` class to define "models" that describe the schema
|
||||||
each table in a database::
|
for each table in a database::
|
||||||
|
|
||||||
class Movie:
|
class Movie:
|
||||||
table = 'Movies' # Table name
|
table = 'Movies' # Table name
|
||||||
|
@ -650,10 +648,13 @@ it can be updated::
|
||||||
>>> Movie('Star Wars').director
|
>>> Movie('Star Wars').director
|
||||||
'J.J. Abrams'
|
'J.J. Abrams'
|
||||||
|
|
||||||
|
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 packaged into individual function
|
use cases are so common that they have been prepackaged into builtin tools.
|
||||||
calls. Properties, bound methods, static methods, and class methods are all
|
Properties, bound methods, static methods, and class methods are all based on
|
||||||
based on the descriptor protocol.
|
the descriptor protocol.
|
||||||
|
|
||||||
|
|
||||||
Properties
|
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
|
Methods can be created manually with :class:`types.MethodType` which is
|
||||||
roughly equivalent to::
|
roughly equivalent to::
|
||||||
|
|
||||||
class Method:
|
class MethodType:
|
||||||
"Emulate Py_MethodType in Objects/classobject.c"
|
"Emulate Py_MethodType in Objects/classobject.c"
|
||||||
|
|
||||||
def __init__(self, func, obj):
|
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"
|
"Simulate func_descr_get() in Objects/funcobject.c"
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self
|
return self
|
||||||
return types.MethodType(self, obj)
|
return MethodType(self, obj)
|
||||||
|
|
||||||
Running the following 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::
|
||||||
|
@ -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!
|
*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
|
Non-data descriptors provide a simple mechanism for variations on the usual
|
||||||
patterns of binding functions into methods.
|
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):
|
def __get__(self, obj, objtype=None):
|
||||||
return self.f
|
return self.f
|
||||||
|
|
||||||
|
|
||||||
|
Class Methods
|
||||||
|
-------------
|
||||||
|
|
||||||
Unlike static methods, class methods prepend the class reference to the
|
Unlike static methods, class methods prepend the class reference to the
|
||||||
argument list before calling the function. This format is the same
|
argument list before calling the function. This format is the same
|
||||||
for whether the caller is an object or a class::
|
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))
|
>>> print(F().f(3))
|
||||||
('F', 3)
|
('F', 3)
|
||||||
|
|
||||||
|
This behavior is useful whenever the method only needs to have a class
|
||||||
This behavior is useful whenever the function only needs to have a class
|
reference and does rely on data stored in a specific instance. One use for
|
||||||
reference and does not care about any underlying data. One use for
|
class methods is to create alternate class constructors. For example, the
|
||||||
class methods is to create alternate class constructors. The classmethod
|
classmethod :func:`dict.fromkeys` creates a new dictionary from a list of
|
||||||
:func:`dict.fromkeys` creates a new dictionary from a list of keys. The pure
|
keys. The pure Python equivalent is::
|
||||||
Python equivalent is::
|
|
||||||
|
|
||||||
class Dict:
|
class Dict:
|
||||||
...
|
...
|
||||||
|
@ -934,7 +938,7 @@ Using the non-data descriptor protocol, a pure Python version of
|
||||||
cls = type(obj)
|
cls = type(obj)
|
||||||
if hasattr(obj, '__get__'):
|
if hasattr(obj, '__get__'):
|
||||||
return self.f.__get__(cls)
|
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
|
The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and
|
||||||
makes it possible for :func:`classmethod` to support chained decorators.
|
makes it possible for :func:`classmethod` to support chained decorators.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue