1573 lines
48 KiB
ReStructuredText
1573 lines
48 KiB
ReStructuredText
.. _descriptorhowto:
|
|
|
|
======================
|
|
Descriptor HowTo Guide
|
|
======================
|
|
|
|
:Author: Raymond Hettinger
|
|
:Contact: <python at rcn dot com>
|
|
|
|
.. Contents::
|
|
|
|
|
|
:term:`Descriptors <descriptor>` let objects customize attribute lookup,
|
|
storage, and deletion.
|
|
|
|
This guide has four major sections:
|
|
|
|
1) The "primer" gives a basic overview, moving gently from simple examples,
|
|
adding one feature at a time. Start here if you're new to descriptors.
|
|
|
|
2) The second section shows a complete, practical descriptor example. If you
|
|
already know the basics, start there.
|
|
|
|
3) The third section provides a more technical tutorial that goes into the
|
|
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 the implementation of common tools like
|
|
:func:`classmethod`, :func:`staticmethod`, :func:`property`, and
|
|
:term:`__slots__`.
|
|
|
|
|
|
Primer
|
|
^^^^^^
|
|
|
|
In this primer, we start with the most basic possible example and then we'll
|
|
add new capabilities one by one.
|
|
|
|
|
|
Simple example: A descriptor that returns a constant
|
|
----------------------------------------------------
|
|
|
|
The :class:`Ten` class is a descriptor that always returns the constant ``10``
|
|
from its :meth:`__get__` method:
|
|
|
|
.. testcode::
|
|
|
|
class Ten:
|
|
def __get__(self, obj, objtype=None):
|
|
return 10
|
|
|
|
To use the descriptor, it must be stored as a class variable in another class:
|
|
|
|
.. testcode::
|
|
|
|
class A:
|
|
x = 5 # Regular class attribute
|
|
y = Ten() # Descriptor instance
|
|
|
|
An interactive session shows the difference between normal attribute lookup
|
|
and descriptor lookup:
|
|
|
|
.. doctest::
|
|
|
|
>>> a = A() # Make an instance of class A
|
|
>>> a.x # Normal attribute lookup
|
|
5
|
|
>>> a.y # Descriptor lookup
|
|
10
|
|
|
|
In the ``a.x`` attribute lookup, the dot operator finds the key ``x`` and the
|
|
value ``5`` in the class dictionary. In the ``a.y`` lookup, the dot operator
|
|
finds a descriptor instance, recognized by its ``__get__`` method, and calls
|
|
that method which returns ``10``.
|
|
|
|
Note that the value ``10`` is not stored in either the class dictionary or the
|
|
instance dictionary. Instead, the value ``10`` is computed on demand.
|
|
|
|
This example shows how a simple descriptor works, but it isn't very useful.
|
|
For retrieving constants, normal attribute lookup would be better.
|
|
|
|
In the next section, we'll create something more useful, a dynamic lookup.
|
|
|
|
|
|
Dynamic lookups
|
|
---------------
|
|
|
|
Interesting descriptors typically run computations instead of returning
|
|
constants:
|
|
|
|
.. testcode::
|
|
|
|
import os
|
|
|
|
class DirectorySize:
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
return len(os.listdir(obj.dirname))
|
|
|
|
class Directory:
|
|
|
|
size = DirectorySize() # Descriptor instance
|
|
|
|
def __init__(self, dirname):
|
|
self.dirname = dirname # Regular instance attribute
|
|
|
|
An interactive session shows that the lookup is dynamic — it computes
|
|
different, updated answers each time::
|
|
|
|
>>> s = Directory('songs')
|
|
>>> g = Directory('games')
|
|
>>> s.size # The songs directory has twenty files
|
|
20
|
|
>>> g.size # The games directory has three files
|
|
3
|
|
>>> open('games/newfile').close() # Add a fourth file to the directory
|
|
>>> g.size # File count is automatically updated
|
|
4
|
|
|
|
Besides showing how descriptors can run computations, this example also
|
|
reveals the purpose of the parameters to :meth:`__get__`. The *self*
|
|
parameter is *size*, an instance of *DirectorySize*. The *obj* parameter is
|
|
either *g* or *s*, an instance of *Directory*. It is the *obj* parameter that
|
|
lets the :meth:`__get__` method learn the target directory. The *objtype*
|
|
parameter is the class *Directory*.
|
|
|
|
|
|
Managed attributes
|
|
------------------
|
|
|
|
A popular use for descriptors is managing access to instance data. The
|
|
descriptor is assigned to a public attribute in the class dictionary while the
|
|
actual data is stored as a private attribute in the instance dictionary. The
|
|
descriptor's :meth:`__get__` and :meth:`__set__` methods are triggered when
|
|
the public attribute is accessed.
|
|
|
|
In the following example, *age* is the public attribute and *_age* is the
|
|
private attribute. When the public attribute is accessed, the descriptor logs
|
|
the lookup or update:
|
|
|
|
.. testcode::
|
|
|
|
import logging
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
class LoggedAgeAccess:
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
value = obj._age
|
|
logging.info('Accessing %r giving %r', 'age', value)
|
|
return value
|
|
|
|
def __set__(self, obj, value):
|
|
logging.info('Updating %r to %r', 'age', value)
|
|
obj._age = value
|
|
|
|
class Person:
|
|
|
|
age = LoggedAgeAccess() # Descriptor instance
|
|
|
|
def __init__(self, name, age):
|
|
self.name = name # Regular instance attribute
|
|
self.age = age # Calls __set__()
|
|
|
|
def birthday(self):
|
|
self.age += 1 # Calls both __get__() and __set__()
|
|
|
|
|
|
An interactive session shows that all access to the managed attribute *age* is
|
|
logged, but that the regular attribute *name* is not logged:
|
|
|
|
.. testcode::
|
|
:hide:
|
|
|
|
import logging, sys
|
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout, force=True)
|
|
|
|
.. doctest::
|
|
|
|
>>> mary = Person('Mary M', 30) # The initial age update is logged
|
|
INFO:root:Updating 'age' to 30
|
|
>>> dave = Person('David D', 40)
|
|
INFO:root:Updating 'age' to 40
|
|
|
|
>>> vars(mary) # The actual data is in a private attribute
|
|
{'name': 'Mary M', '_age': 30}
|
|
>>> vars(dave)
|
|
{'name': 'David D', '_age': 40}
|
|
|
|
>>> mary.age # Access the data and log the lookup
|
|
INFO:root:Accessing 'age' giving 30
|
|
30
|
|
>>> mary.birthday() # Updates are logged as well
|
|
INFO:root:Accessing 'age' giving 30
|
|
INFO:root:Updating 'age' to 31
|
|
|
|
>>> dave.name # Regular attribute lookup isn't logged
|
|
'David D'
|
|
>>> dave.age # Only the managed attribute is logged
|
|
INFO:root:Accessing 'age' giving 40
|
|
40
|
|
|
|
One major issue with this example is that the private name *_age* is hardwired in
|
|
the *LoggedAgeAccess* class. That means that each instance can only have one
|
|
logged attribute and that its name is unchangeable. In the next example,
|
|
we'll fix that problem.
|
|
|
|
|
|
Customized names
|
|
----------------
|
|
|
|
When a class uses descriptors, it can inform each descriptor about which
|
|
variable name was used.
|
|
|
|
In this example, the :class:`Person` class has two descriptor instances,
|
|
*name* and *age*. When the :class:`Person` class is defined, it makes a
|
|
callback to :meth:`__set_name__` in *LoggedAccess* so that the field names can
|
|
be recorded, giving each descriptor its own *public_name* and *private_name*:
|
|
|
|
.. testcode::
|
|
|
|
import logging
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
class LoggedAccess:
|
|
|
|
def __set_name__(self, owner, name):
|
|
self.public_name = name
|
|
self.private_name = '_' + name
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
value = getattr(obj, self.private_name)
|
|
logging.info('Accessing %r giving %r', self.public_name, value)
|
|
return value
|
|
|
|
def __set__(self, obj, value):
|
|
logging.info('Updating %r to %r', self.public_name, value)
|
|
setattr(obj, self.private_name, value)
|
|
|
|
class Person:
|
|
|
|
name = LoggedAccess() # First descriptor instance
|
|
age = LoggedAccess() # Second descriptor instance
|
|
|
|
def __init__(self, name, age):
|
|
self.name = name # Calls the first descriptor
|
|
self.age = age # Calls the second descriptor
|
|
|
|
def birthday(self):
|
|
self.age += 1
|
|
|
|
An interactive session shows that the :class:`Person` class has called
|
|
:meth:`__set_name__` so that the field names would be recorded. Here
|
|
we call :func:`vars` to look up the descriptor without triggering it:
|
|
|
|
.. doctest::
|
|
|
|
>>> vars(vars(Person)['name'])
|
|
{'public_name': 'name', 'private_name': '_name'}
|
|
>>> vars(vars(Person)['age'])
|
|
{'public_name': 'age', 'private_name': '_age'}
|
|
|
|
The new class now logs access to both *name* and *age*:
|
|
|
|
.. testcode::
|
|
:hide:
|
|
|
|
import logging, sys
|
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout, force=True)
|
|
|
|
.. doctest::
|
|
|
|
>>> pete = Person('Peter P', 10)
|
|
INFO:root:Updating 'name' to 'Peter P'
|
|
INFO:root:Updating 'age' to 10
|
|
>>> kate = Person('Catherine C', 20)
|
|
INFO:root:Updating 'name' to 'Catherine C'
|
|
INFO:root:Updating 'age' to 20
|
|
|
|
The two *Person* instances contain only the private names::
|
|
|
|
>>> vars(pete)
|
|
{'_name': 'Peter P', '_age': 10}
|
|
>>> vars(kate)
|
|
{'_name': 'Catherine C', '_age': 20}
|
|
|
|
|
|
Closing thoughts
|
|
----------------
|
|
|
|
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 was
|
|
created or the name of class variable it was assigned to. (This method, if
|
|
present, is called even if the class is not a descriptor.)
|
|
|
|
Descriptors get invoked by the dot "operator" during attribute lookup. If a
|
|
descriptor is accessed indirectly with ``vars(some_class)[descriptor_name]``,
|
|
the descriptor instance is returned without invoking it.
|
|
|
|
Descriptors only work when used as class variables. When put in instances,
|
|
they have no effect.
|
|
|
|
The main motivation for descriptors is to provide a hook allowing objects
|
|
stored in class variables to control what happens during attribute lookup.
|
|
|
|
Traditionally, the calling class controls what happens during lookup.
|
|
Descriptors invert that relationship and allow the data being looked-up to
|
|
have a say in the matter.
|
|
|
|
Descriptors are used throughout the language. It is how functions turn into
|
|
bound methods. Common tools like :func:`classmethod`, :func:`staticmethod`,
|
|
:func:`property`, and :func:`functools.cached_property` are all implemented as
|
|
descriptors.
|
|
|
|
|
|
Complete Practical Example
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
In this example, we create a practical and powerful tool for locating
|
|
notoriously hard to find data corruption bugs.
|
|
|
|
|
|
Validator class
|
|
---------------
|
|
|
|
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
|
|
restrictions. If those restrictions aren't met, it raises an exception to
|
|
prevent data corruption at its source.
|
|
|
|
This :class:`Validator` class is both an :term:`abstract base class` and a
|
|
managed attribute descriptor:
|
|
|
|
.. testcode::
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
class Validator(ABC):
|
|
|
|
def __set_name__(self, owner, name):
|
|
self.private_name = '_' + name
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
return getattr(obj, self.private_name)
|
|
|
|
def __set__(self, obj, value):
|
|
self.validate(value)
|
|
setattr(obj, self.private_name, value)
|
|
|
|
@abstractmethod
|
|
def validate(self, value):
|
|
pass
|
|
|
|
Custom validators need to inherit from :class:`Validator` and must supply a
|
|
:meth:`validate` method to test various restrictions as needed.
|
|
|
|
|
|
Custom validators
|
|
-----------------
|
|
|
|
Here are three practical data validation utilities:
|
|
|
|
1) :class:`OneOf` verifies that a value is one of a restricted set of options.
|
|
|
|
2) :class:`Number` verifies that a value is either an :class:`int` or
|
|
:class:`float`. Optionally, it verifies that a value is between a given
|
|
minimum or maximum.
|
|
|
|
3) :class:`String` verifies that a value is a :class:`str`. Optionally, it
|
|
validates a given minimum or maximum length. It can validate a
|
|
user-defined `predicate
|
|
<https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)>`_ as well.
|
|
|
|
.. testcode::
|
|
|
|
class OneOf(Validator):
|
|
|
|
def __init__(self, *options):
|
|
self.options = set(options)
|
|
|
|
def validate(self, value):
|
|
if value not in self.options:
|
|
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
|
|
|
|
class Number(Validator):
|
|
|
|
def __init__(self, minvalue=None, maxvalue=None):
|
|
self.minvalue = minvalue
|
|
self.maxvalue = maxvalue
|
|
|
|
def validate(self, value):
|
|
if not isinstance(value, (int, float)):
|
|
raise TypeError(f'Expected {value!r} to be an int or float')
|
|
if self.minvalue is not None and value < self.minvalue:
|
|
raise ValueError(
|
|
f'Expected {value!r} to be at least {self.minvalue!r}'
|
|
)
|
|
if self.maxvalue is not None and value > self.maxvalue:
|
|
raise ValueError(
|
|
f'Expected {value!r} to be no more than {self.maxvalue!r}'
|
|
)
|
|
|
|
class String(Validator):
|
|
|
|
def __init__(self, minsize=None, maxsize=None, predicate=None):
|
|
self.minsize = minsize
|
|
self.maxsize = maxsize
|
|
self.predicate = predicate
|
|
|
|
def validate(self, value):
|
|
if not isinstance(value, str):
|
|
raise TypeError(f'Expected {value!r} to be an str')
|
|
if self.minsize is not None and len(value) < self.minsize:
|
|
raise ValueError(
|
|
f'Expected {value!r} to be no smaller than {self.minsize!r}'
|
|
)
|
|
if self.maxsize is not None and len(value) > self.maxsize:
|
|
raise ValueError(
|
|
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
|
|
)
|
|
if self.predicate is not None and not self.predicate(value):
|
|
raise ValueError(
|
|
f'Expected {self.predicate} to be true for {value!r}'
|
|
)
|
|
|
|
|
|
Practical application
|
|
---------------------
|
|
|
|
Here's how the data validators can be used in a real class:
|
|
|
|
.. testcode::
|
|
|
|
class Component:
|
|
|
|
name = String(minsize=3, maxsize=10, predicate=str.isupper)
|
|
kind = OneOf('wood', 'metal', 'plastic')
|
|
quantity = Number(minvalue=0)
|
|
|
|
def __init__(self, name, kind, quantity):
|
|
self.name = name
|
|
self.kind = kind
|
|
self.quantity = quantity
|
|
|
|
The descriptors prevent invalid instances from being created:
|
|
|
|
.. doctest::
|
|
|
|
>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
|
|
|
|
>>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
|
|
|
|
>>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Expected -5 to be at least 0
|
|
>>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
|
|
Traceback (most recent call last):
|
|
...
|
|
TypeError: Expected 'V' to be an int or float
|
|
|
|
>>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid
|
|
|
|
|
|
Technical Tutorial
|
|
^^^^^^^^^^^^^^^^^^
|
|
|
|
What follows is a more technical tutorial for the mechanics and details of how
|
|
descriptors work.
|
|
|
|
|
|
Abstract
|
|
--------
|
|
|
|
Defines descriptors, summarizes the protocol, and shows how descriptors are
|
|
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.
|
|
|
|
|
|
Definition and introduction
|
|
---------------------------
|
|
|
|
In general, a descriptor is an attribute value that has one of the methods in
|
|
the descriptor protocol. Those methods are :meth:`__get__`, :meth:`__set__`,
|
|
and :meth:`__delete__`. If any of those methods are defined for an the
|
|
attribute, it is said to be a :term:`descriptor`.
|
|
|
|
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
|
|
starting with ``a.__dict__['x']``, then ``type(a).__dict__['x']``, and
|
|
continuing through the method resolution order of ``type(a)``. If the
|
|
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.
|
|
Where this occurs in the precedence chain depends on which descriptor methods
|
|
were defined.
|
|
|
|
Descriptors are a powerful, general purpose protocol. They are the mechanism
|
|
behind properties, methods, static methods, class methods, and
|
|
:func:`super()`. They are used throughout Python itself. Descriptors
|
|
simplify the underlying C code and offer a flexible set of new tools for
|
|
everyday Python programs.
|
|
|
|
|
|
Descriptor protocol
|
|
-------------------
|
|
|
|
``descr.__get__(self, obj, type=None) -> value``
|
|
|
|
``descr.__set__(self, obj, value) -> None``
|
|
|
|
``descr.__delete__(self, obj) -> None``
|
|
|
|
That is all there is to it. Define any of these methods and an object is
|
|
considered a descriptor and can override default behavior upon being looked up
|
|
as an attribute.
|
|
|
|
If an object defines :meth:`__set__` or :meth:`__delete__`, it is considered
|
|
a data descriptor. Descriptors that only define :meth:`__get__` are called
|
|
non-data descriptors (they are often used for methods but other uses are
|
|
possible).
|
|
|
|
Data and non-data descriptors differ in how overrides are calculated with
|
|
respect to entries in an instance's dictionary. If an instance's dictionary
|
|
has an entry with the same name as a data descriptor, the data descriptor
|
|
takes precedence. If an instance's dictionary has an entry with the same
|
|
name as a non-data descriptor, the dictionary entry takes precedence.
|
|
|
|
To make a read-only data descriptor, define both :meth:`__get__` and
|
|
:meth:`__set__` with the :meth:`__set__` raising an :exc:`AttributeError` when
|
|
called. Defining the :meth:`__set__` method with an exception raising
|
|
placeholder is enough to make it a data descriptor.
|
|
|
|
|
|
Overview of descriptor invocation
|
|
---------------------------------
|
|
|
|
A descriptor can be called directly with ``desc.__get__(obj)`` or
|
|
``desc.__get__(None, cls)``.
|
|
|
|
But it is more common for a descriptor to be invoked automatically from
|
|
attribute access.
|
|
|
|
The expression ``obj.x`` looks up the attribute ``x`` in the chain of
|
|
namespaces for ``obj``. If the search finds a descriptor outside of the
|
|
instance ``__dict__``, 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
|
|
instance of super.
|
|
|
|
|
|
Invocation from an instance
|
|
---------------------------
|
|
|
|
Instance lookup scans through a chain of namespaces giving data descriptors
|
|
the highest priority, followed by instance variables, then non-data
|
|
descriptors, then class variables, and lastly :meth:`__getattr__` if it is
|
|
provided.
|
|
|
|
If a descriptor is found for ``a.x``, then it is invoked with:
|
|
``desc.__get__(a, type(a))``.
|
|
|
|
The logic for a dotted lookup is in :meth:`object.__getattribute__`. Here is
|
|
a pure Python equivalent:
|
|
|
|
.. testcode::
|
|
|
|
def object_getattribute(obj, name):
|
|
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
|
|
null = object()
|
|
objtype = type(obj)
|
|
cls_var = getattr(objtype, name, null)
|
|
descr_get = getattr(type(cls_var), '__get__', null)
|
|
if descr_get is not null:
|
|
if (hasattr(type(cls_var), '__set__')
|
|
or hasattr(type(cls_var), '__delete__')):
|
|
return descr_get(cls_var, obj, objtype) # data descriptor
|
|
if hasattr(obj, '__dict__') and name in vars(obj):
|
|
return vars(obj)[name] # instance variable
|
|
if descr_get is not null:
|
|
return descr_get(cls_var, obj, objtype) # non-data descriptor
|
|
if cls_var is not null:
|
|
return cls_var # class variable
|
|
raise AttributeError(name)
|
|
|
|
|
|
.. testcode::
|
|
:hide:
|
|
|
|
# Test the fidelity of object_getattribute() by comparing it with the
|
|
# normal object.__getattribute__(). The former will be accessed by
|
|
# square brackets and the latter by the dot operator.
|
|
|
|
class Object:
|
|
|
|
def __getitem__(obj, name):
|
|
try:
|
|
return object_getattribute(obj, name)
|
|
except AttributeError:
|
|
if not hasattr(type(obj), '__getattr__'):
|
|
raise
|
|
return type(obj).__getattr__(obj, name) # __getattr__
|
|
|
|
class DualOperator(Object):
|
|
|
|
x = 10
|
|
|
|
def __init__(self, z):
|
|
self.z = z
|
|
|
|
@property
|
|
def p2(self):
|
|
return 2 * self.x
|
|
|
|
@property
|
|
def p3(self):
|
|
return 3 * self.x
|
|
|
|
def m5(self, y):
|
|
return 5 * y
|
|
|
|
def m7(self, y):
|
|
return 7 * y
|
|
|
|
def __getattr__(self, name):
|
|
return ('getattr_hook', self, name)
|
|
|
|
class DualOperatorWithSlots:
|
|
|
|
__getitem__ = Object.__getitem__
|
|
|
|
__slots__ = ['z']
|
|
|
|
x = 15
|
|
|
|
def __init__(self, z):
|
|
self.z = z
|
|
|
|
@property
|
|
def p2(self):
|
|
return 2 * self.x
|
|
|
|
def m5(self, y):
|
|
return 5 * y
|
|
|
|
def __getattr__(self, name):
|
|
return ('getattr_hook', self, name)
|
|
|
|
|
|
.. doctest::
|
|
:hide:
|
|
|
|
>>> a = DualOperator(11)
|
|
>>> vars(a).update(p3 = '_p3', m7 = '_m7')
|
|
>>> a.x == a['x'] == 10
|
|
True
|
|
>>> a.z == a['z'] == 11
|
|
True
|
|
>>> a.p2 == a['p2'] == 20
|
|
True
|
|
>>> a.p3 == a['p3'] == 30
|
|
True
|
|
>>> a.m5(100) == a.m5(100) == 500
|
|
True
|
|
>>> a.m7 == a['m7'] == '_m7'
|
|
True
|
|
>>> a.g == a['g'] == ('getattr_hook', a, 'g')
|
|
True
|
|
|
|
>>> b = DualOperatorWithSlots(22)
|
|
>>> b.x == b['x'] == 15
|
|
True
|
|
>>> b.z == b['z'] == 22
|
|
True
|
|
>>> b.p2 == b['p2'] == 30
|
|
True
|
|
>>> b.m5(200) == b['m5'](200) == 1000
|
|
True
|
|
>>> b.g == b['g'] == ('getattr_hook', b, 'g')
|
|
True
|
|
|
|
|
|
Interestingly, attribute lookup doesn't call :meth:`object.__getattribute__`
|
|
directly. Instead, both the dot operator and the :func:`getattr` function
|
|
perform attribute lookup by way of a helper function:
|
|
|
|
.. testcode::
|
|
|
|
def getattr_hook(obj, name):
|
|
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
|
|
try:
|
|
return obj.__getattribute__(name)
|
|
except AttributeError:
|
|
if not hasattr(type(obj), '__getattr__'):
|
|
raise
|
|
return type(obj).__getattr__(obj, name) # __getattr__
|
|
|
|
So if :meth:`__getattr__` exists, it is called whenever :meth:`__getattribute__`
|
|
raises :exc:`AttributeError` (either directly or in one of the descriptor calls).
|
|
|
|
Also, if a user calls :meth:`object.__getattribute__` directly, the
|
|
:meth:`__getattr__` hook is bypassed entirely.
|
|
|
|
|
|
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()`.
|
|
|
|
A dotted lookup such as ``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.
|
|
|
|
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
|
|
`Guido's Tutorial
|
|
<https://www.python.org/download/releases/2.2.3/descrintro/#cooperation>`_.
|
|
|
|
|
|
Summary of invocation logic
|
|
---------------------------
|
|
|
|
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:
|
|
|
|
* 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
|
|
---------------------------
|
|
|
|
Sometimes it is desirable for a descriptor to know what class variable name it
|
|
was assigned to. When a new class is created, the :class:`type` metaclass
|
|
scans the dictionary of the new class. If any of the entries are descriptors
|
|
and if they define :meth:`__set_name__`, that method is called with two
|
|
arguments. The *owner* is the class where the descriptor is used, and the
|
|
*name* is the class variable the descriptor was assigned to.
|
|
|
|
The implementation details are in :c:func:`type_new()` and
|
|
:c:func:`set_names()` in :source:`Objects/typeobject.c`.
|
|
|
|
Since the update logic is in :meth:`type.__new__`, notifications only take
|
|
place at the time of class creation. If descriptors are added to the class
|
|
afterwards, :meth:`__set_name__` will need to be called manually.
|
|
|
|
|
|
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 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:
|
|
|
|
.. testcode::
|
|
|
|
class Field:
|
|
|
|
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):
|
|
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
|
|
|
|
def __set__(self, obj, value):
|
|
conn.execute(self.store, [value, obj.key])
|
|
conn.commit()
|
|
|
|
We can use the :class:`Field` class to define `models
|
|
<https://en.wikipedia.org/wiki/Database_model>`_ that describe the schema for
|
|
each table in a database:
|
|
|
|
.. testcode::
|
|
|
|
class Movie:
|
|
table = 'Movies' # Table name
|
|
key = 'title' # Primary key
|
|
director = Field()
|
|
year = Field()
|
|
|
|
def __init__(self, key):
|
|
self.key = key
|
|
|
|
class Song:
|
|
table = 'Music'
|
|
key = 'title'
|
|
artist = Field()
|
|
year = Field()
|
|
genre = Field()
|
|
|
|
def __init__(self, key):
|
|
self.key = key
|
|
|
|
To use the models, first connect to the database::
|
|
|
|
>>> import sqlite3
|
|
>>> conn = sqlite3.connect('entertainment.db')
|
|
|
|
An interactive session shows how data is retrieved from the database and how
|
|
it can be updated:
|
|
|
|
.. testsetup::
|
|
|
|
song_data = [
|
|
('Country Roads', 'John Denver', 1972),
|
|
('Me and Bobby McGee', 'Janice Joplin', 1971),
|
|
('Coal Miners Daughter', 'Loretta Lynn', 1970),
|
|
]
|
|
|
|
movie_data = [
|
|
('Star Wars', 'George Lucas', 1977),
|
|
('Jaws', 'Steven Spielberg', 1975),
|
|
('Aliens', 'James Cameron', 1986),
|
|
]
|
|
|
|
import sqlite3
|
|
|
|
conn = sqlite3.connect(':memory:')
|
|
conn.execute('CREATE TABLE Music (title text, artist text, year integer);')
|
|
conn.execute('CREATE INDEX MusicNdx ON Music (title);')
|
|
conn.executemany('INSERT INTO Music VALUES (?, ?, ?);', song_data)
|
|
conn.execute('CREATE TABLE Movies (title text, director text, year integer);')
|
|
conn.execute('CREATE INDEX MovieNdx ON Music (title);')
|
|
conn.executemany('INSERT INTO Movies VALUES (?, ?, ?);', movie_data)
|
|
conn.commit()
|
|
|
|
.. doctest::
|
|
|
|
>>> 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'
|
|
|
|
|
|
Pure Python Equivalents
|
|
^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The descriptor protocol is simple and offers exciting possibilities. Several
|
|
use cases are so common that they have been prepackaged into built-in tools.
|
|
Properties, bound methods, static methods, class methods, and \_\_slots\_\_ are
|
|
all based on the descriptor protocol.
|
|
|
|
|
|
Properties
|
|
----------
|
|
|
|
Calling :func:`property` is a succinct way of building a data descriptor that
|
|
triggers a function call upon access to an attribute. Its signature is::
|
|
|
|
property(fget=None, fset=None, fdel=None, doc=None) -> property
|
|
|
|
The documentation shows a typical use to define a managed attribute ``x``:
|
|
|
|
.. testcode::
|
|
|
|
class C:
|
|
def getx(self): return self.__x
|
|
def setx(self, value): self.__x = value
|
|
def delx(self): del self.__x
|
|
x = property(getx, setx, delx, "I'm the 'x' property.")
|
|
|
|
To see how :func:`property` is implemented in terms of the descriptor protocol,
|
|
here is a pure Python equivalent:
|
|
|
|
.. testcode::
|
|
|
|
class Property:
|
|
"Emulate PyProperty_Type() in Objects/descrobject.c"
|
|
|
|
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
|
|
self.fget = fget
|
|
self.fset = fset
|
|
self.fdel = fdel
|
|
if doc is None and fget is not None:
|
|
doc = fget.__doc__
|
|
self.__doc__ = doc
|
|
self._name = ''
|
|
|
|
def __set_name__(self, owner, name):
|
|
self._name = name
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
if obj is None:
|
|
return self
|
|
if self.fget is None:
|
|
raise AttributeError(f'unreadable attribute {self._name}')
|
|
return self.fget(obj)
|
|
|
|
def __set__(self, obj, value):
|
|
if self.fset is None:
|
|
raise AttributeError(f"can't set attribute {self._name}")
|
|
self.fset(obj, value)
|
|
|
|
def __delete__(self, obj):
|
|
if self.fdel is None:
|
|
raise AttributeError(f"can't delete attribute {self._name}")
|
|
self.fdel(obj)
|
|
|
|
def getter(self, fget):
|
|
prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
|
|
prop._name = self._name
|
|
return prop
|
|
|
|
def setter(self, fset):
|
|
prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
|
|
prop._name = self._name
|
|
return prop
|
|
|
|
def deleter(self, fdel):
|
|
prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
|
|
prop._name = self._name
|
|
return prop
|
|
|
|
.. testcode::
|
|
:hide:
|
|
|
|
# Verify the Property() emulation
|
|
|
|
class CC:
|
|
def getx(self):
|
|
return self.__x
|
|
def setx(self, value):
|
|
self.__x = value
|
|
def delx(self):
|
|
del self.__x
|
|
x = Property(getx, setx, delx, "I'm the 'x' property.")
|
|
|
|
# Now do it again but use the decorator style
|
|
|
|
class CCC:
|
|
@Property
|
|
def x(self):
|
|
return self.__x
|
|
@x.setter
|
|
def x(self, value):
|
|
self.__x = value
|
|
@x.deleter
|
|
def x(self):
|
|
del self.__x
|
|
|
|
|
|
.. doctest::
|
|
:hide:
|
|
|
|
>>> cc = CC()
|
|
>>> hasattr(cc, 'x')
|
|
False
|
|
>>> cc.x = 33
|
|
>>> cc.x
|
|
33
|
|
>>> del cc.x
|
|
>>> hasattr(cc, 'x')
|
|
False
|
|
|
|
>>> ccc = CCC()
|
|
>>> hasattr(ccc, 'x')
|
|
False
|
|
>>> ccc.x = 333
|
|
>>> ccc.x == 333
|
|
True
|
|
>>> del ccc.x
|
|
>>> hasattr(ccc, 'x')
|
|
False
|
|
|
|
The :func:`property` builtin helps whenever a user interface has granted
|
|
attribute access and then subsequent changes require the intervention of a
|
|
method.
|
|
|
|
For instance, a spreadsheet class may grant access to a cell value through
|
|
``Cell('b10').value``. Subsequent improvements to the program require the cell
|
|
to be recalculated on every access; however, the programmer does not want to
|
|
affect existing client code accessing the attribute directly. The solution is
|
|
to wrap access to the value attribute in a property data descriptor:
|
|
|
|
.. testcode::
|
|
|
|
class Cell:
|
|
...
|
|
|
|
@property
|
|
def value(self):
|
|
"Recalculate the cell before returning value"
|
|
self.recalc()
|
|
return self._value
|
|
|
|
Either the built-in :func:`property` or our :func:`Property` equivalent would
|
|
work in this example.
|
|
|
|
|
|
Functions and methods
|
|
---------------------
|
|
|
|
Python's object oriented features are built upon a function based environment.
|
|
Using non-data descriptors, the two are merged seamlessly.
|
|
|
|
Functions stored in class dictionaries get turned into methods when invoked.
|
|
Methods only differ from regular functions in that the object instance is
|
|
prepended to the other arguments. By convention, the instance is called
|
|
*self* but could be called *this* or any other variable name.
|
|
|
|
Methods can be created manually with :class:`types.MethodType` which is
|
|
roughly equivalent to:
|
|
|
|
.. testcode::
|
|
|
|
class MethodType:
|
|
"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 that return bound methods
|
|
during dotted lookup from an instance. Here's how it works:
|
|
|
|
.. testcode::
|
|
|
|
class Function:
|
|
...
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
"Simulate func_descr_get() in Objects/funcobject.c"
|
|
if obj is None:
|
|
return self
|
|
return MethodType(self, obj)
|
|
|
|
Running the following class in the interpreter shows how the function
|
|
descriptor works in practice:
|
|
|
|
.. testcode::
|
|
|
|
class D:
|
|
def f(self, x):
|
|
return x
|
|
|
|
The function has a :term:`qualified name` attribute to support introspection:
|
|
|
|
.. doctest::
|
|
|
|
>>> 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']
|
|
<function D.f at 0x00C45070>
|
|
|
|
Dotted access from a class calls :meth:`__get__` which just returns the
|
|
underlying function unchanged::
|
|
|
|
>>> D.f
|
|
<function D.f at 0x00C45070>
|
|
|
|
The interesting behavior occurs during dotted access from an instance. The
|
|
dotted lookup calls :meth:`__get__` which returns a bound method object::
|
|
|
|
>>> d = D()
|
|
>>> d.f
|
|
<bound method D.f of <__main__.D object at 0x00B18C90>>
|
|
|
|
Internally, the bound method stores the underlying function and the bound
|
|
instance::
|
|
|
|
>>> d.f.__func__
|
|
<function D.f at 0x00C45070>
|
|
|
|
>>> d.f.__self__
|
|
<__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
|
|
--------------
|
|
|
|
Non-data descriptors provide a simple mechanism for variations on the usual
|
|
patterns of binding functions into methods.
|
|
|
|
To recap, functions have a :meth:`__get__` method so that they can be converted
|
|
to a method when accessed as attributes. The non-data descriptor transforms an
|
|
``obj.f(*args)`` call into ``f(obj, *args)``. Calling ``cls.f(*args)``
|
|
becomes ``f(*args)``.
|
|
|
|
This chart summarizes the binding and its two most useful variants:
|
|
|
|
+-----------------+----------------------+------------------+
|
|
| Transformation | Called from an | Called from a |
|
|
| | object | class |
|
|
+=================+======================+==================+
|
|
| function | f(obj, \*args) | f(\*args) |
|
|
+-----------------+----------------------+------------------+
|
|
| staticmethod | f(\*args) | f(\*args) |
|
|
+-----------------+----------------------+------------------+
|
|
| classmethod | f(type(obj), \*args) | f(cls, \*args) |
|
|
+-----------------+----------------------+------------------+
|
|
|
|
Static methods return the underlying function without changes. Calling either
|
|
``c.f`` or ``C.f`` is the equivalent of a direct lookup into
|
|
``object.__getattribute__(c, "f")`` or ``object.__getattribute__(C, "f")``. As a
|
|
result, the function becomes identically accessible from either an object or a
|
|
class.
|
|
|
|
Good candidates for static methods are methods that do not reference the
|
|
``self`` variable.
|
|
|
|
For instance, a statistics package may include a container class for
|
|
experimental data. The class provides normal methods for computing the average,
|
|
mean, median, and other descriptive statistics that depend on the data. However,
|
|
there may be useful functions which are conceptually related but do not depend
|
|
on the data. For instance, ``erf(x)`` is handy conversion routine that comes up
|
|
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
|
|
``Sample.erf(1.5) --> .9332``.
|
|
|
|
Since static methods return the underlying function with no changes, the
|
|
example calls are unexciting:
|
|
|
|
.. testcode::
|
|
|
|
class E:
|
|
@staticmethod
|
|
def f(x):
|
|
print(x)
|
|
|
|
.. doctest::
|
|
|
|
>>> E.f(3)
|
|
3
|
|
>>> E().f(3)
|
|
3
|
|
|
|
Using the non-data descriptor protocol, a pure Python version of
|
|
:func:`staticmethod` would look like this:
|
|
|
|
.. doctest::
|
|
|
|
class StaticMethod:
|
|
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
|
|
|
|
def __init__(self, f):
|
|
self.f = f
|
|
|
|
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:
|
|
|
|
.. testcode::
|
|
|
|
class F:
|
|
@classmethod
|
|
def f(cls, x):
|
|
return cls.__name__, x
|
|
|
|
.. doctest::
|
|
|
|
>>> F.f(3)
|
|
('F', 3)
|
|
>>> F().f(3)
|
|
('F', 3)
|
|
|
|
This behavior is useful whenever the method only needs to have a class
|
|
reference and does not 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:
|
|
|
|
.. testcode::
|
|
|
|
class Dict(dict):
|
|
@classmethod
|
|
def fromkeys(cls, iterable, value=None):
|
|
"Emulate dict_fromkeys() in Objects/dictobject.c"
|
|
d = cls()
|
|
for key in iterable:
|
|
d[key] = value
|
|
return d
|
|
|
|
Now a new dictionary of unique keys can be constructed like this:
|
|
|
|
.. doctest::
|
|
|
|
>>> d = Dict.fromkeys('abracadabra')
|
|
>>> type(d) is Dict
|
|
True
|
|
>>> d
|
|
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
|
|
|
|
Using the non-data descriptor protocol, a pure Python version of
|
|
:func:`classmethod` would look like this:
|
|
|
|
.. testcode::
|
|
|
|
class ClassMethod:
|
|
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
|
|
|
|
def __init__(self, f):
|
|
self.f = f
|
|
|
|
def __get__(self, obj, cls=None):
|
|
if cls is None:
|
|
cls = type(obj)
|
|
if hasattr(obj, '__get__'):
|
|
return self.f.__get__(cls)
|
|
return MethodType(self.f, cls)
|
|
|
|
.. testcode::
|
|
:hide:
|
|
|
|
# Verify the emulation works
|
|
class T:
|
|
@ClassMethod
|
|
def cm(cls, x, y):
|
|
return (cls, x, y)
|
|
|
|
.. doctest::
|
|
:hide:
|
|
|
|
>>> T.cm(11, 22)
|
|
(<class 'T'>, 11, 22)
|
|
|
|
# Also call it from an instance
|
|
>>> t = T()
|
|
>>> t.cm(11, 22)
|
|
(<class 'T'>, 11, 22)
|
|
|
|
The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and
|
|
makes it possible for :func:`classmethod` to support chained decorators.
|
|
For example, a classmethod and property could be chained together:
|
|
|
|
.. testcode::
|
|
|
|
class G:
|
|
@classmethod
|
|
@property
|
|
def __doc__(cls):
|
|
return f'A doc for {cls.__name__!r}'
|
|
|
|
.. doctest::
|
|
|
|
>>> G.__doc__
|
|
"A doc for 'G'"
|
|
|
|
|
|
Member objects and __slots__
|
|
----------------------------
|
|
|
|
When a class defines ``__slots__``, it replaces instance dictionaries with a
|
|
fixed-length array of slot values. From a user point of view that has
|
|
several effects:
|
|
|
|
1. Provides immediate detection of bugs due to misspelled attribute
|
|
assignments. Only attribute names specified in ``__slots__`` are allowed:
|
|
|
|
.. testcode::
|
|
|
|
class Vehicle:
|
|
__slots__ = ('id_number', 'make', 'model')
|
|
|
|
.. doctest::
|
|
|
|
>>> auto = Vehicle()
|
|
>>> auto.id_nubmer = 'VYE483814LQEX'
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
|
|
|
|
2. Helps create immutable objects where descriptors manage access to private
|
|
attributes stored in ``__slots__``:
|
|
|
|
.. testcode::
|
|
|
|
class Immutable:
|
|
|
|
__slots__ = ('_dept', '_name') # Replace the instance dictionary
|
|
|
|
def __init__(self, dept, name):
|
|
self._dept = dept # Store to private attribute
|
|
self._name = name # Store to private attribute
|
|
|
|
@property # Read-only descriptor
|
|
def dept(self):
|
|
return self._dept
|
|
|
|
@property
|
|
def name(self): # Read-only descriptor
|
|
return self._name
|
|
|
|
.. doctest::
|
|
|
|
>>> mark = Immutable('Botany', 'Mark Watney')
|
|
>>> mark.dept
|
|
'Botany'
|
|
>>> mark.dept = 'Space Pirate'
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: can't set attribute
|
|
>>> mark.location = 'Mars'
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: 'Immutable' object has no attribute 'location'
|
|
|
|
3. Saves memory. On a 64-bit Linux build, an instance with two attributes
|
|
takes 48 bytes with ``__slots__`` and 152 bytes without. This `flyweight
|
|
design pattern <https://en.wikipedia.org/wiki/Flyweight_pattern>`_ likely only
|
|
matters when a large number of instances are going to be created.
|
|
|
|
4. Blocks tools like :func:`functools.cached_property` which require an
|
|
instance dictionary to function correctly:
|
|
|
|
.. testcode::
|
|
|
|
from functools import cached_property
|
|
|
|
class CP:
|
|
__slots__ = () # Eliminates the instance dict
|
|
|
|
@cached_property # Requires an instance dict
|
|
def pi(self):
|
|
return 4 * sum((-1.0)**n / (2.0*n + 1.0)
|
|
for n in reversed(range(100_000)))
|
|
|
|
.. doctest::
|
|
|
|
>>> CP().pi
|
|
Traceback (most recent call last):
|
|
...
|
|
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
|
|
|
|
It is not possible to create an exact drop-in pure Python version of
|
|
``__slots__`` because it requires direct access to C structures and control
|
|
over object memory allocation. However, we can build a mostly faithful
|
|
simulation where the actual C structure for slots is emulated by a private
|
|
``_slotvalues`` list. Reads and writes to that private structure are managed
|
|
by member descriptors:
|
|
|
|
.. testcode::
|
|
|
|
null = object()
|
|
|
|
class Member:
|
|
|
|
def __init__(self, name, clsname, offset):
|
|
'Emulate PyMemberDef in Include/structmember.h'
|
|
# Also see descr_new() in Objects/descrobject.c
|
|
self.name = name
|
|
self.clsname = clsname
|
|
self.offset = offset
|
|
|
|
def __get__(self, obj, objtype=None):
|
|
'Emulate member_get() in Objects/descrobject.c'
|
|
# Also see PyMember_GetOne() in Python/structmember.c
|
|
value = obj._slotvalues[self.offset]
|
|
if value is null:
|
|
raise AttributeError(self.name)
|
|
return value
|
|
|
|
def __set__(self, obj, value):
|
|
'Emulate member_set() in Objects/descrobject.c'
|
|
obj._slotvalues[self.offset] = value
|
|
|
|
def __delete__(self, obj):
|
|
'Emulate member_delete() in Objects/descrobject.c'
|
|
value = obj._slotvalues[self.offset]
|
|
if value is null:
|
|
raise AttributeError(self.name)
|
|
obj._slotvalues[self.offset] = null
|
|
|
|
def __repr__(self):
|
|
'Emulate member_repr() in Objects/descrobject.c'
|
|
return f'<Member {self.name!r} of {self.clsname!r}>'
|
|
|
|
The :meth:`type.__new__` method takes care of adding member objects to class
|
|
variables:
|
|
|
|
.. testcode::
|
|
|
|
class Type(type):
|
|
'Simulate how the type metaclass adds member objects for slots'
|
|
|
|
def __new__(mcls, clsname, bases, mapping):
|
|
'Emuluate type_new() in Objects/typeobject.c'
|
|
# type_new() calls PyTypeReady() which calls add_methods()
|
|
slot_names = mapping.get('slot_names', [])
|
|
for offset, name in enumerate(slot_names):
|
|
mapping[name] = Member(name, clsname, offset)
|
|
return type.__new__(mcls, clsname, bases, mapping)
|
|
|
|
The :meth:`object.__new__` method takes care of creating instances that have
|
|
slots instead of an instance dictionary. Here is a rough simulation in pure
|
|
Python:
|
|
|
|
.. testcode::
|
|
|
|
class Object:
|
|
'Simulate how object.__new__() allocates memory for __slots__'
|
|
|
|
def __new__(cls, *args):
|
|
'Emulate object_new() in Objects/typeobject.c'
|
|
inst = super().__new__(cls)
|
|
if hasattr(cls, 'slot_names'):
|
|
empty_slots = [null] * len(cls.slot_names)
|
|
object.__setattr__(inst, '_slotvalues', empty_slots)
|
|
return inst
|
|
|
|
def __setattr__(self, name, value):
|
|
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
|
|
cls = type(self)
|
|
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
|
|
raise AttributeError(
|
|
f'{type(self).__name__!r} object has no attribute {name!r}'
|
|
)
|
|
super().__setattr__(name, value)
|
|
|
|
def __delattr__(self, name):
|
|
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
|
|
cls = type(self)
|
|
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
|
|
raise AttributeError(
|
|
f'{type(self).__name__!r} object has no attribute {name!r}'
|
|
)
|
|
super().__delattr__(name)
|
|
|
|
To use the simulation in a real class, just inherit from :class:`Object` and
|
|
set the :term:`metaclass` to :class:`Type`:
|
|
|
|
.. testcode::
|
|
|
|
class H(Object, metaclass=Type):
|
|
'Instance variables stored in slots'
|
|
|
|
slot_names = ['x', 'y']
|
|
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
At this point, the metaclass has loaded member objects for *x* and *y*::
|
|
|
|
>>> from pprint import pp
|
|
>>> pp(dict(vars(H)))
|
|
{'__module__': '__main__',
|
|
'__doc__': 'Instance variables stored in slots',
|
|
'slot_names': ['x', 'y'],
|
|
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
|
|
'x': <Member 'x' of 'H'>,
|
|
'y': <Member 'y' of 'H'>}
|
|
|
|
.. doctest::
|
|
:hide:
|
|
|
|
# We test this separately because the preceding section is not
|
|
# doctestable due to the hex memory address for the __init__ function
|
|
>>> isinstance(vars(H)['x'], Member)
|
|
True
|
|
>>> isinstance(vars(H)['y'], Member)
|
|
True
|
|
|
|
When instances are created, they have a ``slot_values`` list where the
|
|
attributes are stored:
|
|
|
|
.. doctest::
|
|
|
|
>>> h = H(10, 20)
|
|
>>> vars(h)
|
|
{'_slotvalues': [10, 20]}
|
|
>>> h.x = 55
|
|
>>> vars(h)
|
|
{'_slotvalues': [55, 20]}
|
|
|
|
Misspelled or unassigned attributes will raise an exception:
|
|
|
|
.. doctest::
|
|
|
|
>>> h.xz
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: 'H' object has no attribute 'xz'
|
|
|
|
.. doctest::
|
|
:hide:
|
|
|
|
# Examples for deleted attributes are not shown because this section
|
|
# is already a bit lengthy. We still test that code here.
|
|
>>> del h.x
|
|
>>> hasattr(h, 'x')
|
|
False
|
|
|
|
# Also test the code for uninitialized slots
|
|
>>> class HU(Object, metaclass=Type):
|
|
... slot_names = ['x', 'y']
|
|
...
|
|
>>> hu = HU()
|
|
>>> hasattr(hu, 'x')
|
|
False
|
|
>>> hasattr(hu, 'y')
|
|
False
|