3
Advanced Topics
The preceding sections provide enough information for you
to use traits for manifestly-typed attributes, with initialization and
validation. This section describes the advanced features of the Traits package,
including custom trait handlers, notification, and delegation. It also delves
into advanced aspects of initialization and validation.
3.1
Custom Trait Handlers
If you need a trait that cannot be defined using the
standard set of trait handling classes, you can create your own subclass of TraitHandler. The constructor (i.e., __init__ method) for your TraitHandler subclass can accept whatever
additional information, if any, is needed to completely specify the trait. The
constructor does not need to call the TraitHandler
base class’s constructor.
The only method that a custom trait handler must implement is validate().
Refer to the Traits API Reference for details about
this function.
In many cases, it is also necessary to override one or
more of the following methods in a TraitHandler
subclass:
info(self)
post_setattr(self, object, name, value)
get_editor(self, trait)
3.1.1
Example Custom Trait Handler
To illustrate the process of creating a TraitHandler subclass, the following is a
definition of a trait handler that only allows positive, odd integers as legal
values:
#
custom_traithandler.py --- Example of a custom TraitHandler
import
types
from
enthought.traits.api import TraitHandler
class
TraitOddInteger(TraitHandler):
def validate(self,
object, name, value):
if
((type(value) is types.IntType) and
(value
> 0) and ((value % 2) == 1)):
return
value
self.error(object,
name, value)
def info(self):
return 'a
positive odd integer'
An application could use this new trait handler to define
traits such as the following:
# use_custom_th.py ---
Example of using a custom TraitHandler
from
enthought.traits.api import HasTraits, Trait, TraitRange
from
custom_traithandler import TraitOddInteger
class
AnOddClass(HasTraits):
oddball = Trait(1,
TraitOddInteger())
very_odd =
Trait(-1, TraitOddInteger(),
TraitRange(-10, -1))
The following example demonstrates why the info() method
returns a phrase rather than a complete sentence:
>>> from
use_custom_th import AnOddClass
>>> odd_stuff
= AnOddClass()
>>>
odd_stuff.very_odd = 0
Traceback (most recent
call last):
File
"test.py", line 25, in ?
odd_stuff.very_odd
= 0
File
"C:\cvsroot\traits\traits.py", line 1119, in validate
raise TraitError,
excp
traits.traits.TraitError:
The 'very_odd' trait of a AnOddClass instance must be a
positive odd integer or an integer in the range from -10 to -1, but a
value of 0 was specified.
Note the emphasized result returned by the info() method, which is embedded in the exception
generated by the invalid assignment.
3.2
Trait Notification
When the value of an attribute changes, other parts of the
program might need to be notified that the change has occurred. The Traits
package makes this possible for trait attributes. This functionality lets you
write programs using the same, powerful event-driven model that is used in
writing user interfaces and for other problem domains.
Requesting trait attribute
change notifications is done in one of two ways:
· Statically,
by writing methods using a special naming convention in the class defining the
trait attribute whose change notification is to be handled.
· Dynamically,
by calling either on_trait_change() or on_trait_event() to establish (or remove) change
notification handlers.
3.2.1
Static Notification
The static approach is the most convenient, but it is not
always possible to use it. Writing a static change notification handler
requires that, for a class whose trait attribute changes you are interested in,
you write a method with a special name on that class (or a subclass).
There are two kinds of special method names that can be
used for static trait attribute change notifications. One is
attribute-specific, and the other applies to all trait attributes on a class.
To notify about changes to a trait attribute named name, define a method named _name_changed() or _name_fired(). The
leading underscore indicates that attribute-specific notification handlers are
normally part of a class's private API. Methods named _name_fired() are normally used with traits that are
events, described in Section 3.2.3, “Trait Events”.
To notify about changes to any trait attribute on a class,
define a method named _anytrait_changed().
Both of these types of static trait attribute notification
methods are illustrated in the following example:
#
static_notification.py --- Example of static attribute
#
notification
from
enthought.traits.api import HasTraits, Trait
class
Person(HasTraits):
weight_kg =
Trait(0.0)
height_m =
Trait(1.0)
bmi = Trait(0.0)
def _weight_kg_changed(self, old, new):
print 'weight_kg changed from %s to %s ' % (old, new)
if self.weight.kg != 0.0
self.bmi
= self.weight_kg / (self.height_m**2)
def _anytrait_changed(self,
name, old, new):
print 'The %s
trait changed from %s to %s ' \
%
(name, old, new)
"""
>>> bob =
Person()
>>>
bob.height_m = 1.75
The height_m trait
changed from 1.0 to 1.75
>>>
bob.weight_kg = 100.0
The weight_kg trait
changed from 0.0 to 100.0
weight_kg changed from
0.0 to 100.0
The bmi trait changed
from 0.0 to 32.6530612245
"""
In this example, the attribute-specific notification
function is _weight_kg_changed(), which is
called only when the weight_kg attribute
changes. The class-specific notification handler is _anytrait_changed(), and is called when weight_kg, height_m,
or bmi changes. Thus, both handlers are called when the weight_kg attribute changes. Also, the _weight_kg_changed() function modifies the bmi attribute, which causes _anytrait_changed() to be called for that
attribute.
The arguments passed to the trait attribute change
notification method depend on the method signature and on which type of static
notification handler it is.
For an attribute specific notification handler, the method
signatures supported are:
_name_changed(self)
_name_changed(self, new)
_name_changed(self, old, new)
_name_changed(self, name, old, new)
The method name can also be _name_fired(), with
the same set of signatures.
In these signatures:
· new is the new value assigned to the trait attribute.
· old is the old value assigned to the trait attribute.
· name is the name of the trait attribute.
You can choose whatever method signature from this list is
most convenient to use.
In the case of a non-attribute specific handler, the
method signatures supported are:
_anytrait_changed(self)
_anytrait_changed(self, name)
_anytrait_changed(self, name, new)
_anytrait_changed(self, name, old, new)
The meanings for name, new, and old are the same
as for attribute-specific notification functions.
3.2.2
Dynamic Notification
In cases where a notification handler cannot be defined on
the class (or a subclass) whose trait attribute changes are to be monitored,
you can use dynamic notification instead. In this case, you define a handler
method, and then invoke the on_trait_change()
or on_trait_event() method register that
handler with the object being monitored. The handler registration methods have
the following signatures:
obj.on_trait_change(handler,
name=None, remove=False)
obj.on_trait_event(handler,
name=None, remove=False)
The handler parameter
specifies the function or bound method to be called whenever the name attribute of obj is
modified. If name is None or omitted, handler is called whenever any trait attribute of obj
is modified. If remove is True (or non-zero), then handler
will no longer be called when the name (or any)
trait attribute of obj is modified. In other
words, it causes the handler to be “unhooked” from the event.
Setting up a dynamic trait attribute change notification
handler is illustrated in the following example:
# dynamic_notification
--- Example of dynamic notification
from
enthought.traits.api import Float, HasTraits, Trait
class Part
(HasTraits):
cost = Trait(0.0)
class
Widget (HasTraits):
part1 = Trait(Part)
part2 = Trait(Part)
cost = Float(0.0)
def __init__(self):
self.part1 =
Part()
self.part2 =
Part()
self.part1.on_trait_change(self.update_cost, 'cost')
self.part2.on_trait_change(self.update_cost, 'cost')
def
update_cost(self):
self.cost =
self.part1.cost + self.part2.cost
"""
>>> w =
Widget()
>>>
w.part1.cost = 2.25
>>>
w.part2.cost = 5.31
>>> print
w.cost
7.56
"""
In this example, the Widget
constructor sets up a dynamic trait attribute change notification so that its update_cost() method is called whenever the cost attribute of either its part1 or part2
attributes is modified.
The handler passed to on_trait_change()
or on_trait_event() can have any one of the
following signatures:
handler()
handler(new)
handler(name, new)
handler(object, name, new)
handler(object, name, old, new)
Unlike the static trait attribute change notification
handlers, the signature of a dynamic handler does not depend upon whether the
handler is attribute-specific.
3.2.3
Trait Events
The Traits package defines a special type of trait called
an event. Events are created using the Event()
function, which accepts all of the same arguments as the Trait() function.
There are two major differences between a normal trait and
an event:
· All
notification handlers associated with an event are called whenever any value is assigned to the event. A normal trait
attribute only calls its associated notification handlers when the previous
value of the attribute is different from the new value being assigned to it.
· An event
does not use any storage, and in fact does not store the values assigned to it.
Any value assigned to an event is reported as the new
value to all associated notification handlers, and then immediately discarded.
Because events do not retain a value, the old
argument to a notification handler associated with an event is always the
special Undefined object (see Section 3.2.4).
Similarly, attempting to read the value of an event results in a TraitError exception, because an event has no
value.
As an example of an event, consider:
# event.py --- Example
of trait event
from
enthought.traits.api import Event, HasTraits, List, Tuple
point_2d = Tuple(0, 0)
class
Line2D(HasTraits):
points =
List(point_2d)
line_color =
RGBAColor('black')
updated = Event
def redraw():
pass # Not
implemented for this example
def
_points_changed():
self.updated =
True
def
_updated_fired():
self.redraw()
In support of the use of events, the Traits package
understands attribute-specific notification handlers with names of the form _name_fired(), with signatures identical to the _name_changed() functions. In fact, the Traits package
does not check whether the trait attributes that _name_fired()
handlers are applied to are actually events. The function names are simply
synonyms for programmer convenience.
Similarly, a function named on_trait_event()
can be used as a synonym for on_trait_change()
for dynamic notification.
3.2.4
Undefined Object
Python defines a special, singleton object called None. The Traits package introduces an additional
special, singleton object called Undefined.
The Undefined object
is used to indicate that a trait attribute has not yet had a value set (i.e.,
its value is undefined). Undefined is used
instead of None, because None is often used for other meanings, such as
that the value is not used. In particular, when a trait attribute is first
assigned a value and its associated trait notification handlers are called, Undefined is passed as the value of the old parameter to each handler, to indicate that the
attribute previously had no value. Similarly, the value of a trait event is
always Undefined.
3.3
Trait Delegation
One of the advanced capabilities of the Traits package is
its ability to delegate the definition and default value of a trait attribute
to another object than the one the attribute is defined on. This has many
applications, especially in cases where objects are logically contained within
other objects and may wish to inherit or derive some attributes from the object
they are contained in or associated with. Delegation leverages the common “has-a”
relationship between objects, rather than the “is-a” relationship that class
inheritance provides.
Trait attributes based on delegation are defined using the
Delegate() function, rather than the Trait() function.
3.3.1
Delegate() Function
The signature of the
Delegate function is:
Delegate(delegate, prefix='', modify=False)
The delegate parameter is a
string that specifies the name of the object attribute that refers to the
trait’s delegate. The current value of the trait attribute defined by delegate is used as the delegate whenever the delegate
is needed. Therefore, if the attribute referenced by delegate
changes its value, the delegation also changes to use the object referenced by
the new value. The prefix and modify parameters to the Delegate()
function specify additional information about how to do the delegation.
If prefix is the empty
string or omitted, the delegation is to an attribute of the delegate object
with the same name as the trait defined by the Delegate()
function. Consider the following example:
# delegate.py ---
Example of trait delegation
from
enthought.traits.api import Delegate, HasTraits, Str, Trait
class
Parent(HasTraits):
first_name = Str
last_name = Str
class
Child(HasTraits):
first_name =
Trait('')
last_name =
Delegate('father')
father =
Trait(Parent)
mother =
Trait(Parent)
"""
>>> tony =
Parent(first_name='Anthony', last_name='Jones')
>>> alice =
Parent(first_name='Alice', last_name='Smith')
>>> sally =
Child( first_name='Sally', father=tony, mother=alice)
>>> print
sally.last_name
Jones
>>>
sally.last_name = 'Smith'
>>>
sally.last_name = sally.mother # ERR: string expected
Traceback (most recent
call last):
File
"<stdin>", line 1, in ?
File
"c:\wrk\src\lib\enthought\traits\trait_handlers.py", line
90, in error
raise TraitError,
( object, name, self.info(), value )
enthought.traits.trait_errors.TraitError:
The 'last_name' trait of a Child instance must be a value of type 'str', but a
value of <__main__.Parent object at 0x009DD6F0> was specified.
"""
A Child object
delegates its last_name attribute to its
father object’s last_name attribute.
Because the prefix parameter was not specified in
the Delegate() function used to define the Child class’s last_name
attribute, the attribute name of the delegate is the same as the original
attribute name. Thus, by default, the last_name
of a Child is the same as the last_name of its father.
Note, however, that once a value is explicitly assigned to
the last_name attribute of a Child, it takes on the explicitly assigned value.
This behavior is illustrated in the example when Sally’s last name is set to be
the same as her mother’s last name. However, delegation still affects the type
of values that can be assigned to the last_name
attribute, as illustrated in the example by the attempt to assign Sally’s
mother (an object) as Sally’s last name. The last_name
attribute delegates the assignment to the father
trait, whose last_name attribute specifies
that the only legal values are strings.
3.3.1.1
Prefix Keyword
When the prefix parameter to
the Delegate() function is a non-empty
string, the rule for performing trait attribute look-up in the delegated to
object is modified, with the modification depending on the format of the prefix
string:
· If prefix is a valid Python attribute name, then the
original attribute name is replaced by prefix
when performing the delegate object attribute look-up.
· If prefix ends with an asterisk ('*'), and is longer than
one character, then prefix, minus the trailing
asterisk, is added to the front of the original attribute name when performing
the delegate object trait look-up.
· If prefix is equal to a single asterisk ('*'), the value
of the object class’s __prefix__ attribute
is added to the front of the original attribute name when performing the
delegate object trait look-up.
Each of these three possibilities is illustrated in the
following example:
# delegate_prefix.py
--- Examples of Delegate() prefix parameter
from
enthought.traits.api import \
Delegate, Float,
HasTraits, Str, Trait
class
Parent (HasTraits):
first_name = Str
family_name = ''
favorite_first_name = Str
child_allowance =
Float(1.00)
class Child (HasTraits):
__prefix__ =
'child_'
first_name =
Delegate('mother', 'favorite_*')
last_name =
Delegate('father', 'family_name')
allowance =
Delegate('father', '*')
father =
Trait(Parent)
mother =
Trait(Parent)
In this example, instances of the Child class have three delegated trait
attributes:
· first_name, which delegates to the favorite_first_name attribute of its mother attribute.
· last_name, which delegates to the family_name attribute of its father attribute.
· allowance, which delegates to the child_allowance attribute of its father attribute.
3.3.1.2
Modify Keyword
The final form of delegation occurs when the modify parameter to the Delegate()
function is True. In this case, the
attribute delegates to the attribute specified by the delegate
and prefix parameters as before, but any changes
to the attribute are made to the delegate object’s attribute value, not to the
object delegating the attribute. This form is useful for implementing a proxy
design pattern, where the object using delegation is really a proxy for another
object.
Note that when using delegation, the attribute being
delegated to (such as family_name in the
preceeding example) need not be defined by a trait. That is, the attribute that
is delegated-to can be a standard Python attribute.
3.4
Initialization and Validation Revisited
The following sections present advanced topics related to
the initialization and validation features of the Traits package.
· Dynamic
initialization
· Overriding
default values
· Reusing
trait definitions
· Trait
attribute definition strategies
· Type-checked
methods
3.4.1
Dynamic Initialization
When you use the Trait()
function or other trait factory functions to define traits, you specify their
default values statically. You can also define a method that dynamically
initializes a trait attribute the first time that the attribute value is
accessed. To do this, you define a method with the following signature:
_name_default(self)
This method initializes the name
trait attribute, returning its initial value. The method overrides any default
value specified in the trait definition.
3.4.2
Overriding Default Values in a Subclass
Often, a subclass must override a trait attribute in a
parent class by providing a different default value. You can specify a new
default value without completely re-specifying the trait definition for the
attribute. For example:
# override_default.py
-- Example of overriding a default value for
#
a trait attribute in a subclass
from
enthought.traits.api import HasTraits, Range
class
Employee(HasTraits):
name = Str
salary_grade =
Range(value=1, low=1, high=10)
class
Manager(Employee):
salary_grade = 5
In this example, the salary_grade
of the Employee class is a range from 1 to
10, with a default value of 1. In the Manager
subclass, the default value of salary_grade
is 5, but it is still a range as defined in the Employee
class.
3.4.3
Reusing Trait Definitions
As mentioned in Section 2, “Defining Traits:
Initialization and Validation”, in most cases, traits are defined in-line in
attribute definitions, but they can also be defined independently. A trait
definition only describes the characteristics of a trait, and not the current
value of a trait attribute, so it can be used in the definition of any number
of attributes. For example:
#
trait_reuse.py --- Example of reusing trait definitions
from
enthought.traits.api import \
HasTraits, Range,
Trait, TraitRange
coefficient =
Trait(0.0, TraitRange(-1.0, 1.0))
class
quadratic(HasTraits):
c2 = coefficient
c1 = coefficient
c0 = coefficient
x = Range(-100.0,
100.0, 0.0)
In this example, a trait named coefficient is defined externally to the class quadratic, which references coefficient in the definitions of its trait
attributes c2, c1, and c0.
Each of these attributes has a unique value, but they all use the same trait
definition to determine whether a value assigned to them is valid.
3.4.4
Trait Attribute Definition Strategies
In the preceding examples in this guide, all trait
attribute definitions have bound a single object attribute to a specified trait
definition. This is known as explicit trait attribute definition. The Traits
package supports other strategies for defining trait attributes. You can
associate a category of attributes with a particular trait definition, using
the trait attribute name wildcard. You can also dynamically create trait
attributes that are specific to an instance, using the add_trait() method, rather than defined on a
class. These strategies are described in the following sections.
3.4.4.1
Trait Attribute Name Wildcard
The Traits package enables you to define a category of
trait attributes associated with a particular trait definition, by including an
underscore ('_') as a wildcard at the end of a trait attribute name. For
example:
#
temp_wildcard.py --- Example of using a wildcard with a Trait
#
attribute name
from
enthought.traits.api import Any, HasTraits
class
Person(HasTraits):
temp_ = Any
This example defines a class Person,
with a category of attributes that have names beginning with 'temp', and that
are defined by the Any trait. Thus, any
part of the program that uses a Person
instance can reference attributes such as tempCount,
temp_name, or temp_whatever,
without having to explicitly declare these trait attributes. Each such
attribute has None as the initial value and
allows assignment of any value (because it is based on the Any trait).
You can even give all object attributes a default trait definition,
by specifying only the wildcard character for the attribute name:
# all_wildcard.py ---
Example of trait attribute wildcard rules
from
enthought.traits.api import Any, HasTraits
class
Person(HasTraits):
_ = Any
In this case, all Person
instance attributes can be created on the fly and are defined by the Any trait.
3.4.4.1.1
Wildcard Rules
When using wildcard characters in trait attribute names,
the following rules are used to determine what trait definition governs an
attribute:
1.
If an attribute name exactly matches a name without a wildcard
character, that definition applies.
2.
Otherwise, if an attribute name matches one or more names with wildcard
characters, the definition with the longest name applies.
Note that all possible attribute names are covered by one
of these two rules. The base HasTraits
class implicitly contains the attribute definition _ = Python. This rule guarantees that, by default, all
attributes have standard Python language semantics.
These rules are demonstrated by the following example:
# wildcard_rules.py
--- Example of trait attribute wildcard rules
from
enthought.traits.api import Any, HasTraits, Int, Python
class
Person(HasTraits):
temp_count =
Int(-1)
temp_ = Any
_ =
Python
In this example, the Person
class has a temp_count attribute, which
must be an integer and which has an initial value of -1. Any other attribute
with a name starting with ‘temp’ has an initial value of None and allows any value to be assigned. All
other object attributes behave like normal Python attributes (i.e., they allow
any value to be assigned, but they must have a value assigned to them before
their first reference).
3.4.4.1.2
Disallow Object
The singleton object Disallow
can be used with wildcards to disallow all attributes that are not explicitly
defined. For example:
# disallow.py ---
Example of using Disallow with wildcards
from
enthought.traits.api import \
Disallow, Float,
HasTraits, Int, Str
class Person
(HasTraits):
name = Str
age = Int
weight = Float
_ = Disallow
In this example, a Person instance has three trait
attributes:
· name—Must be a string; its initial value is ''.
· age—Must be an integer; its initial value is 0.
· weight—Must be a float; its initial value is 0.0.
All other object attributes are explicitly disallowed.
That is, any attempt to read or set any object attribute other than name, age,
or weight causes an exception.
3.4.4.1.3
HasTraits Subclasses
Because the HasTraits
class implicitly contains the attribute definition _ = Python, subclasses of
HasTraits by default have very standard
Python attribute behavior for any attribute not explicitly defined as a trait
attribute. However, the wildcard trait attribute definition rules make it easy
to create subclasses of HasTraits with very
non-standard attribute behavior. Two such subclasses are predefined in the
Traits package: HasStrictTraits and HasPrivateTraits.
3.4.4.1.4
HasStrictTraits
This class guarantees that accessing any object attribute
that does not have an explicit or wildcard trait definition results in an
exception. This can be useful in cases where a more rigorous software
engineering approach is employed than is typical for Python programs. It also
helps prevent typos and spelling mistakes in attribute names from going
unnoticed; a misspelled attribute name typically causes an exception. The
definition of HasStrictTraits is the
following:
class
HasStrictTraits(HasTraits):
_ = Disallow
HasStrictTraits can be
used to create type-checked data structures, as in the following example:
class
TreeNode(HasStrictTraits):
left = This
right = This
value = Str
This example defines a TreeNode
class that has three attributes: left, right, and value.
The left and right
attributes can only be references to other instances of TreeNode (or subclasses), while the value attribute must be a string. Attempting to
set other types of values generates an exception, as does attempting to set an
attribute that is not one of the three defined attributes. In essence, TreeNode behaves like a type-checked data
structure.
3.4.4.1.5
HasPrivateTraits
This class is similar to HasStrictTraits,
but allows attributes beginning with '_' to have an initial value of None, and to not be type-checked. This is useful
in cases where a class needs private attributes, which are not part of the
class's public API, to keep track of internal object state. Such attributes do
not need to be type-checked because they are only manipulated by the
(presumably correct) methods of the class itself. The definition of HasPrivateTraits is the following:
class HasPrivateTraits(HasTraits):
__ = Any
_ = Disallow
These subclasses of HasTraits
are provided as a convenience, and their use is completely optional. However,
they do illustrate how easy it is to create subclasses with customized default
attribute behavior if desired.
3.4.4.2
Per-Object Trait Attributes
The Traits package allows you to define dynamic trait
attributes that are object-, rather than class-, specific. This is accomplished
using the add_trait() method of the HasTraits class:
object.add_trait(name, trait)
For example:
#
object_trait_attrs.py --- Example of per-object trait attributes
from
enthought.traits.api import HasTraits, Range
class
GUISlider (HasTraits):
def __init__(self,
eval=None, label='Value',
trait=None, min=0.0, max=1.0,
initial=None, **traits):
HasTraits.__init__(self, **traits)
if trait is
None:
if min
> max:
min,
max = max, min
if initial
is None:
initial = min
elif not
(min <= initial <= max):
initial = [min, max][
abs(initial - min) >
abs(initial - max)]
trait =
Range(min, max, value = initial)
self.add_trait(label, trait)
This example creates a GUISlider
class, whose __init__() method can accept a
string label and either a trait definition or minimum, maximum, and initial
values. If no trait definition is specified, one is constructed based on the max and min values. A
trait attribute whose name is the value of label is added to the object, using
the trait definition (whether specified or constructed). Thus, the label trait
attribute on the GUISlider object is
determined by the calling code, and added in the __init__()
method using add_trait().
You can require that add_trait()
must be used in order to add attributes to a class, by deriving the class from HasStrictTraits (see Section 3.4.4.1.4). When a
class inherits from HasStrictTraits, the
program cannot create a new attribute (either a trait attribute or a regular
attribute) simply by assigning to it, as is normally the case in Python. In
this case, add_trait() is the only way to
create a new attribute for the class outside of the class definition.
3.4.5
Type-Checked Methods
In addition to providing type-checked attributes, the
Traits package also provides the ability to create type-checked methods.
A type-checked method is created by writing a normal
method definition within a class, preceded by a method()
signature function call, as shown in the following example:
#
type_checked_methods.py --- Example of traits-based method type
#
checking
from enthought.traits.api
import HasTraits, method, Tuple
Color = Tuple(int,
int, int, int)
class
Palette(HasTraits):
method(Color, color1=Color, color2=Color)
def blend (self, color1, color2):
return ((color1[0] + color2[0]) / 2,
(color1[1] + color2[1]) / 2,
(color1[2] + color2[2]) / 2,
(color1[3] + color2[3]) / 2 )
method(Color,
Color, Color)
def max (self,
color1, color2):
return (max(
color1[0], color2[0]),
max(
color1[1], color2[1]),
max(
color1[2], color2[2]),
max(
color1[3], color2[3]) )
In this example, Color
is defined to be a trait that accepts tuples of four integer values. The method() signature function appearing before the
definition of the blend() method ensures
that the two arguments to blend() both
match the Color trait definition, as does
the result returned by blend(). The method
signature appearing before the max() method
does exactly the same thing, but uses positional rather than keyword arguments.
When
Use of the method()
signature function is optional. Methods not preceded by a method() function have standard Python behavior
(i.e., no type-checking of arguments or results is performed). Also, the method() function can be used in classes that do
not subclass from HasTraits, because the
resulting method performs the type checking directly. And finally, when the method() function is used, it must directly
precede the definition of the method whose type signature it defines. (However,
white space is allowed.) If it does not, a TraitError
is raised.
3.5
Useful Methods on HasTraits
The HasTraits class
defines a number of methods, which are available to any class derived from it,
i.e., any class that uses trait attributes. This section provides examples of a
sample of these methods. Refer to the Traits API
Reference for a complete list of HasTraits
methods.
3.5.1
add_trait()
This method adds a trait attribute to an object
dynamically, after the object has been created. For more information, see
Section 3.4.4.2, “Per-Object Trait Attributes”.
3.5.2
clone_traits()
This method copies trait attributes from one object to
another. It can copy specified attributes, all explicitly defined trait
attributes, or all explicitly and implicitly defined trait attributes on the
source object.
This method is useful if you want to allow a user to edit
a clone of an object, so that changes are made permanent only when the user commits
them. In such a case, you might clone an object and its trait attributes; allow
the user to modify the clone; and then re-clone only the trait attributes back
to the original object when the user commits changes.
3.5.3
set()
This takes a list of keyword-value pairs, and sets the
trait attribute corresponding to each keyword to the matching value. This
shorthand is useful when a number of trait attributes need to be set on an
object, or a trait attribute value needs to be set in a lambda function. For
example:
person.set(name='Bill', age=27)
The statement above is equivalent to the following:
person.name = 'Bill'
person.age = 27
3.5.4
add_class_trait()
The add_class_trait()
method is a class method, while the preceding HasTraits
methods are instance methods. This method is very similar to the add_trait() instance method. The difference is
that adding a trait attribute by using add_class_trait()
is the same as having declared the trait as part of the class definition. That
is, any trait attribute added using add_class_trait()
is defined in every subsequently-created instance of the class, and in any
subsequently-defined subclasses of the class. In contrast, the add_trait() method adds the specified trait
attribute only to the object instance it is applied to.
In addition, if the name of the trait attribute ends with
a '_', then a new (or replacement) prefix rule is added to the class
definition, just as if the prefix rule had been specified statically in the
class definition. It is not possible to define new prefix rules using the add_trait() method.
One of the main uses of the add_class_trait()
method is to add trait attribute definitions that could not be defined
statically as part of the body of the class definition. This occurs, for
example, when two classes with trait attributes are being defined and each
class has a trait attribute that should contain a reference to the other. For
the class that occurs first in lexical order, it is not possible to define the
trait attribute that references the other class, since the class it needs to
refer to has not yet been defined. This is illustrated in the following
example:
#
circular_definition.py --- Non-working example of mutually-
#
referring classes
from
enthought.traits.api import HasTraits, Trait
class
Chicken(HasTraits):
hatched_from =
Trait(Egg)
class Egg(HasTraits):
created_by =
Trait(Chicken)
As it stands, this example will not run because the hatched_from attribute references the Egg class, which has not yet been defined.
Reversing the definition order of the classes does not fix the problem, because
then the created_by trait references the Chicken class, which has not yet been defined.
The problem can be solved using the add_class_trait()
method, as shown in the following code:
#
add_class_trait.py --- Example of mutually-referring classes
#
using add_class_trait()
from
enthought.traits.api import HasTraits, Trait
class
Chicken(HasTraits):
pass
class Egg(HasTraits):
created_by =
Trait(Chicken)
Chicken.add_class_trait('hatched_from',
Egg)
3.6
Performance Considerations of Traits
Using traits can potentially impose a performance penalty
on attribute access over and above that of normal Python attributes. For the
most part, this penalty, if any, is small, because the core of the Traits
package is written in C, just like the Python interpreter. In fact, for some
common cases, subclasses of HasTraits can
actually have the same or better performance than old or new style Python
classes.
However, there are a couple of performance-related factors
to keep in mind when defining classes and attributes using traits:
· Whether a
trait attribute delegates
· The
complexity of a trait definition
If a trait attribute does not delegate, the performance
penalty can be characterized as follows:
· Getting a
value: No penalty (i.e., standard Python attribute access speed or faster)
· Setting a
value: Depends upon the complexity of the validation tests performed by the
trait definition. Many of the predefined trait handlers defined in the Traits
package support fast C-level validation. For most of these, the cost of
validation is usually negligible. For other trait handlers, with Python-level
validation methods, the cost can be quite a bit higher.
If a trait attribute does delegate, the cases to be
considered are:
· Getting
the default value: Cost of following the delegation chain. The chain is
resolved at the C level, and is quite fast, but its cost is linear with the
number of delegation links that must be followed to find the default value for
the trait.
· Getting
an explicitly assigned value: No penalty (i.e., standard Python attribute
access speed or faster)
· Setting:
Cost of following the delegation chain plus the cost of performing the
validation of the new value. The preceding discussions about delegation chain
following and fast versus slow validation apply here as well.
Note that in the case where delegation modifies the
delegate object, the cost of getting an attribute always includes the cost of
following the delegation chain.
In a typical application scenario, where attributes are
read more often than they are written, and delegation is not used, the impact
of using traits is often minimal, because the only cost occurs when attributes
are assigned and validated.
The worst case scenario occurs when delegation is used
heavily to provide attributes with default values that are seldom changed. In
this case, the cost of frequently following delegation chains may impose a
measurable performance detriment on the application. Of course, this is offset
by the convenience and flexibility provided by the delegation model. As with
any powerful tool, it is best to understand its strengths and weaknesses and
apply that understanding in determining when use of the tool is justified and
appropriate.
|