Add doctests to the descriptor HowTo (GH-23500)

This commit is contained in:
Raymond Hettinger 2020-11-24 20:57:02 -08:00 committed by GitHub
parent ed1a5a5bac
commit 2d44a6bc4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -43,21 +43,26 @@ Simple example: A descriptor that returns a constant
---------------------------------------------------- ----------------------------------------------------
The :class:`Ten` class is a descriptor that always returns the constant ``10`` The :class:`Ten` class is a descriptor that always returns the constant ``10``
from its :meth:`__get__` method:: from its :meth:`__get__` method:
.. testcode::
class Ten: class Ten:
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
return 10 return 10
To use the descriptor, it must be stored as a class variable in another class:: To use the descriptor, it must be stored as a class variable in another class:
.. testcode::
class A: class A:
x = 5 # Regular class attribute x = 5 # Regular class attribute
y = Ten() # Descriptor instance y = Ten() # Descriptor instance
An interactive session shows the difference between normal attribute lookup An interactive session shows the difference between normal attribute lookup
and descriptor lookup:: and descriptor lookup:
.. doctest::
>>> a = A() # Make an instance of class A >>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup >>> a.x # Normal attribute lookup
@ -83,7 +88,9 @@ Dynamic lookups
--------------- ---------------
Interesting descriptors typically run computations instead of returning Interesting descriptors typically run computations instead of returning
constants:: constants:
.. testcode::
import os import os
@ -131,7 +138,9 @@ the public attribute is accessed.
In the following example, *age* is the public attribute and *_age* is the In the following example, *age* is the public attribute and *_age* is the
private attribute. When the public attribute is accessed, the descriptor logs private attribute. When the public attribute is accessed, the descriptor logs
the lookup or update:: the lookup or update:
.. testcode::
import logging import logging
@ -201,7 +210,9 @@ variable name was used.
In this example, the :class:`Person` class has two descriptor instances, In this example, the :class:`Person` class has two descriptor instances,
*name* and *age*. When the :class:`Person` class is defined, it makes a *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 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*:: be recorded, giving each descriptor its own *public_name* and *private_name*:
.. testcode::
import logging import logging
@ -236,7 +247,9 @@ be recorded, giving each descriptor its own *public_name* and *private_name*::
An interactive session shows that the :class:`Person` class has called An interactive session shows that the :class:`Person` class has called
:meth:`__set_name__` so that the field names would be recorded. Here :meth:`__set_name__` so that the field names would be recorded. Here
we call :func:`vars` to look up the descriptor without triggering it:: we call :func:`vars` to look up the descriptor without triggering it:
.. doctest::
>>> vars(vars(Person)['name']) >>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'} {'public_name': 'name', 'private_name': '_name'}
@ -307,7 +320,9 @@ restrictions. If those restrictions aren't met, it raises an exception to
prevent 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:
.. testcode::
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -347,7 +362,7 @@ Here are three practical data validation utilities:
user-defined `predicate user-defined `predicate
<https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)>`_ as well. <https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)>`_ as well.
:: .. testcode::
class OneOf(Validator): class OneOf(Validator):
@ -400,10 +415,12 @@ Here are three practical data validation utilities:
) )
Practical use Practical application
------------- ---------------------
Here's how the data validators can be used in a real class:: Here's how the data validators can be used in a real class:
.. testcode::
class Component: class Component:
@ -418,11 +435,26 @@ Here's how the data validators can be used in a real class::
The descriptors prevent invalid instances from being created:: The descriptors prevent invalid instances from being created::
Component('WIDGET', 'metal', 5) # Allowed. >>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase Traceback (most recent call last):
Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled ...
Component('WIDGET', 'metal', -5) # Blocked: -5 is negative ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
>>> 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 Technical Tutorial
@ -526,7 +558,9 @@ If a descriptor is found for ``a.x``, then it is invoked with:
``desc.__get__(a, type(a))``. ``desc.__get__(a, type(a))``.
The logic for a dotted lookup is in :meth:`object.__getattribute__`. Here is The logic for a dotted lookup is in :meth:`object.__getattribute__`. Here is
a pure Python equivalent:: a pure Python equivalent:
.. testcode::
def object_getattribute(obj, name): def object_getattribute(obj, name):
"Emulate PyObject_GenericGetAttr() in Objects/object.c" "Emulate PyObject_GenericGetAttr() in Objects/object.c"
@ -546,9 +580,108 @@ a pure Python equivalent::
return cls_var # class variable return cls_var # class variable
raise AttributeError(name) 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__` Interestingly, attribute lookup doesn't call :meth:`object.__getattribute__`
directly. Instead, both the dot operator and the :func:`getattr` function directly. Instead, both the dot operator and the :func:`getattr` function
perform attribute lookup by way of a helper function:: perform attribute lookup by way of a helper function:
.. testcode::
def getattr_hook(obj, name): def getattr_hook(obj, name):
"Emulate slot_tp_getattr_hook() in Objects/typeobject.c" "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
@ -650,7 +783,9 @@ be used to implement an `object relational mapping
The essential idea is that the data is stored in an external database. The 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 Python instances only hold keys to the database's tables. Descriptors take
care of lookups or updates:: care of lookups or updates:
.. testcode::
class Field: class Field:
@ -665,8 +800,11 @@ care of lookups or updates::
conn.execute(self.store, [value, obj.key]) conn.execute(self.store, [value, obj.key])
conn.commit() conn.commit()
We can use the :class:`Field` class to define "models" that describe the schema We can use the :class:`Field` class to define `models
for each table in a database:: <https://en.wikipedia.org/wiki/Database_model>`_ that describe the schema for
each table in a database:
.. testcode::
class Movie: class Movie:
table = 'Movies' # Table name table = 'Movies' # Table name
@ -687,12 +825,41 @@ for each table in a database::
def __init__(self, key): def __init__(self, key):
self.key = key self.key = key
An interactive session shows how data is retrieved from the database and how To use the models, first connect to the database::
it can be updated::
>>> import sqlite3 >>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db') >>> 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 >>> Movie('Star Wars').director
'George Lucas' 'George Lucas'
>>> jaws = Movie('Jaws') >>> jaws = Movie('Jaws')
@ -724,7 +891,9 @@ triggers a function call upon access to an attribute. Its signature is::
property(fget=None, fset=None, fdel=None, doc=None) -> property 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``:
.. testcode::
class C: class C:
def getx(self): return self.__x def getx(self): return self.__x
@ -733,7 +902,9 @@ The documentation shows a typical use to define a managed attribute ``x``::
x = property(getx, setx, delx, "I'm the 'x' property.") x = property(getx, setx, delx, "I'm the 'x' property.")
To see how :func:`property` is implemented in terms of the descriptor protocol, To see how :func:`property` is implemented in terms of the descriptor protocol,
here is a pure Python equivalent:: here is a pure Python equivalent:
.. testcode::
class Property: class Property:
"Emulate PyProperty_Type() in Objects/descrobject.c" "Emulate PyProperty_Type() in Objects/descrobject.c"
@ -772,6 +943,57 @@ here is a pure Python equivalent::
def deleter(self, fdel): def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__) return type(self)(self.fget, self.fset, fdel, self.__doc__)
.. 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 The :func:`property` builtin helps whenever a user interface has granted
attribute access and then subsequent changes require the intervention of a attribute access and then subsequent changes require the intervention of a
method. method.
@ -780,7 +1002,9 @@ For instance, a spreadsheet class may grant access to a cell value through
``Cell('b10').value``. Subsequent improvements to the program require the cell ``Cell('b10').value``. Subsequent improvements to the program require the cell
to be recalculated on every access; however, the programmer does not want to to be recalculated on every access; however, the programmer does not want to
affect existing client code accessing the attribute directly. The solution is affect existing client code accessing the attribute directly. The solution is
to wrap access to the value attribute in a property data descriptor:: to wrap access to the value attribute in a property data descriptor:
.. testcode::
class Cell: class Cell:
... ...
@ -791,6 +1015,9 @@ to wrap access to the value attribute in a property data descriptor::
self.recalc() self.recalc()
return self._value return self._value
Either the built-in :func:`property` or our :func:`Property` equivalent would
work in this example.
Functions and methods Functions and methods
--------------------- ---------------------
@ -804,7 +1031,9 @@ prepended to the other arguments. By convention, the instance is called
*self* but could be called *this* or any other variable name. *self* but could be called *this* or any other variable name.
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:
.. testcode::
class MethodType: class MethodType:
"Emulate Py_MethodType in Objects/classobject.c" "Emulate Py_MethodType in Objects/classobject.c"
@ -821,7 +1050,9 @@ roughly equivalent to::
To support automatic creation of methods, functions include the To support automatic creation of methods, functions include the
:meth:`__get__` method for binding methods during attribute access. This :meth:`__get__` method for binding methods during attribute access. This
means that functions are non-data descriptors that return bound methods means that functions are non-data descriptors that return bound methods
during dotted lookup from an instance. Here's how it works:: during dotted lookup from an instance. Here's how it works:
.. testcode::
class Function: class Function:
... ...
@ -833,13 +1064,17 @@ during dotted lookup from an instance. Here's how it works::
return 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:
.. testcode::
class D: class D:
def f(self, x): def f(self, x):
return x return x
The function has a :term:`qualified name` attribute to support introspection:: The function has a :term:`qualified name` attribute to support introspection:
.. doctest::
>>> D.f.__qualname__ >>> D.f.__qualname__
'D.f' 'D.f'
@ -867,7 +1102,7 @@ Internally, the bound method stores the underlying function and the bound
instance:: instance::
>>> d.f.__func__ >>> d.f.__func__
<function D.f at 0x1012e5ae8> <function D.f at 0x00C45070>
>>> d.f.__self__ >>> d.f.__self__
<__main__.D object at 0x1012e1f98> <__main__.D object at 0x1012e1f98>
@ -919,20 +1154,26 @@ It can be called either from an object or the class: ``s.erf(1.5) --> .9332`` o
``Sample.erf(1.5) --> .9332``. ``Sample.erf(1.5) --> .9332``.
Since static methods return the underlying function with no changes, the Since static methods return the underlying function with no changes, the
example calls are unexciting:: example calls are unexciting:
.. testcode::
class E: class E:
@staticmethod @staticmethod
def f(x): def f(x):
print(x) print(x)
.. doctest::
>>> E.f(3) >>> E.f(3)
3 3
>>> E().f(3) >>> E().f(3)
3 3
Using the non-data descriptor protocol, a pure Python version of Using the non-data descriptor protocol, a pure Python version of
:func:`staticmethod` would look like this:: :func:`staticmethod` would look like this:
.. doctest::
class StaticMethod: class StaticMethod:
"Emulate PyStaticMethod_Type() in Objects/funcobject.c" "Emulate PyStaticMethod_Type() in Objects/funcobject.c"
@ -949,27 +1190,31 @@ 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:
.. testcode::
class F: class F:
@classmethod @classmethod
def f(cls, x): def f(cls, x):
return cls.__name__, x return cls.__name__, x
>>> print(F.f(3)) .. doctest::
>>> F.f(3)
('F', 3) ('F', 3)
>>> print(F().f(3)) >>> 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 method only needs to have a class
reference and does rely on data stored in a specific instance. One use for 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 class methods is to create alternate class constructors. For example, the
classmethod :func:`dict.fromkeys` creates a new dictionary from a list of classmethod :func:`dict.fromkeys` creates a new dictionary from a list of
keys. The pure Python equivalent is:: keys. The pure Python equivalent is:
class Dict: .. testcode::
...
class Dict(dict):
@classmethod @classmethod
def fromkeys(cls, iterable, value=None): def fromkeys(cls, iterable, value=None):
"Emulate dict_fromkeys() in Objects/dictobject.c" "Emulate dict_fromkeys() in Objects/dictobject.c"
@ -978,13 +1223,17 @@ keys. The pure Python equivalent is::
d[key] = value d[key] = value
return d return d
Now a new dictionary of unique keys can be constructed like this:: Now a new dictionary of unique keys can be constructed like this:
.. doctest::
>>> Dict.fromkeys('abracadabra') >>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None} {'a': None, 'b': None, 'r': None, 'c': None, 'd': None}
Using the non-data descriptor protocol, a pure Python version of Using the non-data descriptor protocol, a pure Python version of
:func:`classmethod` would look like this:: :func:`classmethod` would look like this:
.. testcode::
class ClassMethod: class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c" "Emulate PyClassMethod_Type() in Objects/funcobject.c"
@ -999,9 +1248,31 @@ Using the non-data descriptor protocol, a pure Python version of
return self.f.__get__(cls) return self.f.__get__(cls)
return MethodType(self.f, 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 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.
For example, a classmethod and property could be chained together:: For example, a classmethod and property could be chained together:
.. testcode::
class G: class G:
@classmethod @classmethod
@ -1009,6 +1280,12 @@ For example, a classmethod and property could be chained together::
def __doc__(cls): def __doc__(cls):
return f'A doc for {cls.__name__!r}' return f'A doc for {cls.__name__!r}'
.. doctest::
>>> G.__doc__
"A doc for 'G'"
Member objects and __slots__ Member objects and __slots__
---------------------------- ----------------------------
@ -1017,11 +1294,15 @@ fixed-length array of slot values. From a user point of view that has
several effects: several effects:
1. Provides immediate detection of bugs due to misspelled attribute 1. Provides immediate detection of bugs due to misspelled attribute
assignments. Only attribute names specified in ``__slots__`` are allowed:: assignments. Only attribute names specified in ``__slots__`` are allowed:
.. testcode::
class Vehicle: class Vehicle:
__slots__ = ('id_number', 'make', 'model') __slots__ = ('id_number', 'make', 'model')
.. doctest::
>>> auto = Vehicle() >>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX' >>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last): Traceback (most recent call last):
@ -1029,7 +1310,9 @@ assignments. Only attribute names specified in ``__slots__`` are allowed::
AttributeError: 'Vehicle' object has no attribute 'id_nubmer' AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
2. Helps create immutable objects where descriptors manage access to private 2. Helps create immutable objects where descriptors manage access to private
attributes stored in ``__slots__``:: attributes stored in ``__slots__``:
.. testcode::
class Immutable: class Immutable:
@ -1047,7 +1330,19 @@ attributes stored in ``__slots__``::
def name(self): # Read-only descriptor def name(self): # Read-only descriptor
return self._name return self._name
mark = Immutable('Botany', 'Mark Watney') # Create an immutable instance .. 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 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 takes 48 bytes with ``__slots__`` and 152 bytes without. This `flyweight
@ -1055,7 +1350,9 @@ design pattern <https://en.wikipedia.org/wiki/Flyweight_pattern>`_ likely only
matters when a large number of instances are going to be created. matters when a large number of instances are going to be created.
4. Blocks tools like :func:`functools.cached_property` which require an 4. Blocks tools like :func:`functools.cached_property` which require an
instance dictionary to function correctly:: instance dictionary to function correctly:
.. testcode::
from functools import cached_property from functools import cached_property
@ -1067,17 +1364,21 @@ instance dictionary to function correctly::
return 4 * sum((-1.0)**n / (2.0*n + 1.0) return 4 * sum((-1.0)**n / (2.0*n + 1.0)
for n in reversed(range(100_000))) for n in reversed(range(100_000)))
.. doctest::
>>> CP().pi >>> CP().pi
Traceback (most recent call last): Traceback (most recent call last):
... ...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property. TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
It's not possible to create an exact drop-in pure Python version of 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 ``__slots__`` because it requires direct access to C structures and control
over object memory allocation. However, we can build a mostly faithful over object memory allocation. However, we can build a mostly faithful
simulation where the actual C structure for slots is emulated by a private simulation where the actual C structure for slots is emulated by a private
``_slotvalues`` list. Reads and writes to that private structure are managed ``_slotvalues`` list. Reads and writes to that private structure are managed
by member descriptors:: by member descriptors:
.. testcode::
null = object() null = object()
@ -1114,7 +1415,9 @@ by member descriptors::
return f'<Member {self.name!r} of {self.clsname!r}>' return f'<Member {self.name!r} of {self.clsname!r}>'
The :meth:`type.__new__` method takes care of adding member objects to class The :meth:`type.__new__` method takes care of adding member objects to class
variables:: variables:
.. testcode::
class Type(type): class Type(type):
'Simulate how the type metaclass adds member objects for slots' 'Simulate how the type metaclass adds member objects for slots'
@ -1129,7 +1432,9 @@ variables::
The :meth:`object.__new__` method takes care of creating instances that have 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 slots instead of an instance dictionary. Here is a rough simulation in pure
Python:: Python:
.. testcode::
class Object: class Object:
'Simulate how object.__new__() allocates memory for __slots__' 'Simulate how object.__new__() allocates memory for __slots__'
@ -1161,7 +1466,9 @@ Python::
super().__delattr__(name) super().__delattr__(name)
To use the simulation in a real class, just inherit from :class:`Object` and To use the simulation in a real class, just inherit from :class:`Object` and
set the :term:`metaclass` to :class:`Type`:: set the :term:`metaclass` to :class:`Type`:
.. testcode::
class H(Object, metaclass=Type): class H(Object, metaclass=Type):
'Instance variables stored in slots' 'Instance variables stored in slots'
@ -1174,8 +1481,8 @@ set the :term:`metaclass` to :class:`Type`::
At this point, the metaclass has loaded member objects for *x* and *y*:: At this point, the metaclass has loaded member objects for *x* and *y*::
>>> import pprint >>> from pprint import pp
>>> pprint.pp(dict(vars(H))) >>> pp(dict(vars(H)))
{'__module__': '__main__', {'__module__': '__main__',
'__doc__': 'Instance variables stored in slots', '__doc__': 'Instance variables stored in slots',
'slot_names': ['x', 'y'], 'slot_names': ['x', 'y'],
@ -1183,8 +1490,20 @@ At this point, the metaclass has loaded member objects for *x* and *y*::
'x': <Member 'x' of 'H'>, 'x': <Member 'x' of 'H'>,
'y': <Member 'y' 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 When instances are created, they have a ``slot_values`` list where the
attributes are stored:: attributes are stored:
.. doctest::
>>> h = H(10, 20) >>> h = H(10, 20)
>>> vars(h) >>> vars(h)
@ -1193,9 +1512,30 @@ attributes are stored::
>>> vars(h) >>> vars(h)
{'_slotvalues': [55, 20]} {'_slotvalues': [55, 20]}
Misspelled or unassigned attributes will raise an exception:: Misspelled or unassigned attributes will raise an exception:
.. doctest::
>>> h.xz >>> h.xz
Traceback (most recent call last): Traceback (most recent call last):
... ...
AttributeError: 'H' object has no attribute 'xz' 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