| .. _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 | |
| def __get__(self, obj, objtype=None): | |
| if obj is None: | |
| return self | |
| if self.fget is None: | |
| raise AttributeError("unreadable attribute") | |
| return self.fget(obj) | |
| def __set__(self, obj, value): | |
| if self.fset is None: | |
| raise AttributeError("can't set attribute") | |
| self.fset(obj, value) | |
| def __delete__(self, obj): | |
| if self.fdel is None: | |
| raise AttributeError("can't delete attribute") | |
| self.fdel(obj) | |
| def getter(self, fget): | |
| return type(self)(fget, self.fset, self.fdel, self.__doc__) | |
| def setter(self, fset): | |
| return type(self)(self.fget, fset, self.fdel, self.__doc__) | |
| def deleter(self, fdel): | |
| 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 | |
| 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 |