1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
|
.. index:: internals
Traits internals
================
This section of the documentation is intended both for those developing Traits
itself, and for those using Traits who would like a better understanding of
the Traits internals.
Fundamentals
------------
The runtime behavior of Traits is governed by two key classes: |CTrait| and
|HasTraits|. Each of these classes inherits from a superclass implemented in
the |ctraits| C extension module: |CTrait| inherits from |cTrait|, while
|HasTraits| inherits from |CHasTraits|.
The |CHasTraits| and |cTrait| base classes are not intended for direct use and
do not provide a complete, coherent API: that API is provided by their
respective subclasses |HasTraits| and |CTrait|. The existence and precise
semantics of the base classes should be regarded as implementation details.
In what follows, we'll frequently refer to behavior of |HasTraits| and |CTrait|
even when the behavior we're referring to is actually implemented by
|CHasTraits| or |cTrait|.
We'll examine both of these classes, along with some of their hidden state, in
more detail below.
Key class: |HasTraits|
~~~~~~~~~~~~~~~~~~~~~~
The primary purpose of the |HasTraits| class is to override Python's normal
rules for attribute access. It does this by overriding the
``__getattribute__``, ``__setattr__`` and ``__delattr__`` special methods. At C
level, this is done through providing ``tp_getattro`` and ``tp_setattro``
"slots" to the |CHasTraits| ``PyTypeObject``: ``tp_getattro`` controls
attribute retrieval operations, while ``tp_setattro`` is called for both
attribute set and attribute deletion operations.
To support trait get and set operations, a |HasTraits| object (that is, in
normal use, an instance of a user-defined subclass of |HasTraits|) has two key
pieces of state: a dictionary of **class traits**, and a second dictionary of
**instance traits**. Each dictionary is a mapping from trait attribute names to
|CTrait| objects. A |CTrait| object encapsulates the rules for getting and
setting the corresponding attribute, as well as providing an attachment point
for trait notifications.
In addition to these two dictionaries, a |HasTraits| object has a ``__dict__``
that stores attribute values for that object in the normal way.
Analogously to class variables and instance variables in a normal Python class,
the class traits dictionary for a given |HasTraits| subclass is shared between
instances of that class, while the instance traits dictionary is specific to a
particular |HasTraits| instance. (As an internal optimization, the instance
traits dictionary is created on demand when first needed, while the class
traits dictionary is always present for each instance.)
For introspection and debugging purposes, both of these dictionaries can
be retrieved directly, using the |_instance_traits| and |_class_traits|
methods. However, it's not recommended to use either of these methods in
production code, and you should be especially careful when modifying either
of these dictionaries.
For example, consider the following |HasTraits| subclass::
from traits.api import Float, HasTraits, Str
class Ingredient(HasTraits):
""" An ingredient in a recipe listing. """
#: The name of the ingredient.
name = Str()
#: Ingredient quantity.
quantity = Float()
Here's what happens when we create an instance of this ingredient and inspect
the instance and class trait dictionaries::
>>> eggs = Ingredient(name="eggs", quantity=12)
>>> eggs._instance_traits()
{}
>>> eggs._class_traits()
{
'name': <traits.ctrait.CTrait object at 0x1020cd400>,
'quantity': <traits.ctrait.CTrait object at 0x1020cd360>,
'trait_added': <traits.ctrait.CTrait object at 0x1020cd4a0>,
'trait_modified': <traits.ctrait.CTrait object at 0x1020079a0>,
}
>>> eggs.__dict__
{'name': 'eggs', 'quantity': 12.0}
Note that the actual values for the ingredient are stored in the ``__dict__``
as usual, not in the ``CTrait`` objects.
If we create a second ingredient, it shares class traits (but not
instance traits) with the first one::
>>> flour = Ingredient(name="flour", quantity=3.5)
>>> flour._class_traits() is eggs._class_traits()
True
>>> flour._instance_traits() is eggs._instance_traits()
False
Key class: |CTrait|
~~~~~~~~~~~~~~~~~~~
A |CTrait| object has two main purposes:
- It encapsulates the rules for getting and setting a traited attribute on
a |HasTraits| object.
- It provides an attachment point for trait notifiers.
The hidden state for a |CTrait| instance is encapsulated in the
``trait_object`` C ``struct`` in the |ctraits| source. There are several
interesting fields, not all of which are exposed at Python level.
Of particular interest are the ``getattr`` and ``setattr`` fields, which
hold pointers to C functions that act as the entry points for attribute
access via the given trait. See below for a fuller description of attribute
access mechanics.
Attribute retrieval
~~~~~~~~~~~~~~~~~~~
When evaluating ``obj.name`` for a ``HasTraits`` object ``obj``, the following
sequence of steps occurs (see ``has_traits_getattro`` in the C source):
- The ``name`` is looked up in ``obj.__dict__``. If found, the corresponding
value is returned immediately.
- If ``name`` is not found in ``obj.__dict__``, we look first for an instance
trait named ``name``, and then for a class trait named ``name``. Thus an
instance trait with a given name will shadow a class trait with the same
name.
- If a matching trait is found, its ``getattr`` field is invoked to retrieve
the trait's value for the given object.
- If no matching trait is found, we try to access the attribute value
using Python's usual attribute rules (via the ``PyObject_GenericGetAttr``
C-API call).
- Finally, if the ``PyObject_GenericGetAttr`` call fails, we invoke the
**prefix trait** machinery to get a new ``CTrait`` object, and use that
new trait to get a value.
Note that the above sequence of steps applies to method access as well as
attribute access. Note also that there's no mechanism to automatically
search for ``CTrait`` objects in superclasses of the immediate ``HasTraits``
subclass.
Attribute set operations
~~~~~~~~~~~~~~~~~~~~~~~~
The rules for setting an attribute (evaluating ``obj.name = value`` for a
``HasTraits`` object ``obj``) are analogous to those for attribute retrieval.
The starting point is ``has_traits_setattro`` in the source.
- First we look for the name ``name`` in ``obj._instance_traits()``,
and retrieve the corresponding ``CTrait`` instance if present.
- If no matching entry is found, we then look up ``name`` in
``obj._class_traits()``, and again retrieve the corresponding ``CTrait``.
- If still not found, we invoke the **prefix trait** machinery to get a new
``CTrait`` object. By default, this goes through the
``HasTraits.__prefix_trait__`` method (which is implemented in Python), and
this may still fail with an exception.
- If one of the above steps succeeded, we now have a ``CTrait`` object, and
its ``setattr`` function is invoked (passing along the trait object, ``obj``,
``name`` and ``value``) to perform the actual attribute set operation.
Attribute deletion
~~~~~~~~~~~~~~~~~~
Attribute deletion (``del obj.name``) goes through the same code path as
attribute set operations. Most ``CTrait`` types do not permit deletion.
Traits Containers
-----------------
Traits has the ability to watch for changes in standard Python containers:
lists, dictionaries and sets. To achieve this Traits provides special
subclasses of the standard Python classes that can validate elements and can
fire trait notifications when the contents change. These classes are,
respectively, |TraitList|, |TraitDict| and |TraitSet| (not to be confused with
the deprecated Trait Handlers of the same names).
In addition to being able to take an appropriate value to initialize the
container (such as a sequence or mapping), these container subclasses also
take keyword-only arguments for validators (either a single item validator for
|TraitList| and |TraitSet|, or key and value validators for |TraitDict|) and
notifiers.
These classes were introduced in Traits 6.1 and the implementation details
described here are provisional and may change.
Backwards Compatibility
~~~~~~~~~~~~~~~~~~~~~~~
In practice, most traits containers are instances of |TraitListObject|,
|TraitDictObject| and |TraitSetObject| which are subclasses that are
created by the validators of |List|, |Dict| and |Set| traits respectively and
supplied with appropriate element validators that wrap the standard trait
validators so that constructs like ``List(Str)`` can work.
These objects are designed to be API compatible with the classes of the
same name from Traits 6.0 and before, and so have different constructors and
may behave slightly differently from the base classes in some cases.
In particular |TraitListObject| classes can have restrictions on their
length, which is not part of the base |TraitList| API.
All of these backward compatibility classes are strongly tied to a particular
object and trait, and are not designed to operate as stand-alone entities.
These relationships are required to support the `object.*_items`-style event
traits, but complicate the implementation, for example by having to use
weak references to the object, and having to take this additional structure
into account when serializing and deserializing.
It is a long-term goal to phase out the use of these backwards compatibility
classes.
Container Validators
~~~~~~~~~~~~~~~~~~~~
Container validators are callables that are expected to take a single value
and either return a validated value or raise a TraitError. The default
validators simply pass the input value directly through, but validators can
potentially do much more. Validators are expected to be idempotent:
``validate(validate(x)) == validate(x)`` should hold as long as
``validate(x)`` does not raise an error.
For example, the following validator casts the list items to integers,
raising a TraitError if that fails::
def int_validator(value):
try:
return int(value)
except ValueError:
raise TraitError(
"List items must be castable to an int, but a value %r was specified."
% value
)
So if we were to use this as the validator for a |TraitList| we would get the
following behaviour::
>>> int_list = TraitList(item_validator=int_validator)
>>> int_list.append("5")
>>> int_list
[5]
>>> int_list.extend([3, 5, "aaaarrrghh"])
TraitError: List items must be castable to an int, but a value 'aaaarrrghh' was specified.
In Traits 6.1 validation is not done uniformly before performing operations
to keep behaviour the same as Traits 6.0 and earlier, so results of operations can
sometimes be surprising::
>>> int_list.append("6")
>>> int_list.remove("6")
ValueError: list.remove(x): x not in list
This is expected to be resolved in a future traits version by providing clear
guidance to users about when validation is performed, and possibly changing the
behaviour.
Container Notifiers
~~~~~~~~~~~~~~~~~~~
Container objects also have a list of notifiers that fire when the contents of
the container change. Like trait notifiers, container notifiers are low-level
callbacks that are used by the higher-level, more user-friendly observer and
listener systems. They have a fixed signature, which is slightly different for
lists, dicts and sets, but in all cases starts with the container object itself.
Notifiers are called in the order that they appear in the notifiers list and
should not mutate the parameters that they have been passed.
List notifiers must take 4 arguments: the **trait_list** object, the **index**
value or slice that identifies where the change occurred, a list of
**removed** elements, and a list of **added** elements. The |TraitList|
methods make an attempt to normalize indices and slices to make things easier
for notification writers.
Dict notifiers expect 4 arguments: the **trait_dict** object, a dictionary of
**removed** items, a dictionary of **added** elements, and a dictionary of
**changed** items, where the values are the old values held in the keys.
Set notifiers expect 3 arguments: the **trait_set** object, the set of
**removed** elements, and the set of **added** elements.
Users should not usually need to interact with the container notifiers directly,
just as they do not usually need to interact with trait notifiers.
..
# substitutions
.. |_class_traits| replace:: :meth:`~traits.ctraits.CHasTraits._class_traits`
.. |_instance_traits| replace:: :meth:`~traits.ctraits.CHasTraits._instance_traits`
.. |cTrait| replace:: :class:`~traits.ctraits.cTrait`
.. |CTrait| replace:: :class:`~traits.ctrait.CTrait`
.. |ctraits| replace:: :mod:`~traits.ctraits`
.. |CHasTraits| replace:: :class:`~traits.ctraits.CHasTraits`
.. |HasTraits| replace:: :class:`~traits.has_traits.HasTraits`
.. |TraitList| replace:: :class:`~traits.trait_list_object.TraitList`
.. |TraitDict| replace:: :class:`~traits.trait_dict_object.TraitDict`
.. |TraitSet| replace:: :class:`~traits.trait_dict_object.TraitSet`
.. |TraitListObject| replace:: :class:`~traits.trait_list_object.TraitListObject`
.. |TraitDictObject| replace:: :class:`~traits.trait_dict_object.TraitDictObject`
.. |TraitSetObject| replace:: :class:`~traits.trait_dict_object.TraitSetObject`
.. |List| replace:: :class:`~traits.trait_types.List`
.. |Dict| replace:: :class:`~traits.trait_types.Dict`
.. |Set| replace:: :class:`~traits.trait_types.Set`
|