Traits: A New Way of Adding Properties

To Python Classes

 

David C. Morrill

Enthought, Inc.

 


Introduction

To begin, let’s start with a couple of simple questions and their answers:

 

  • What is a trait?
  • Why would we want to use traits?

 

What is a trait?

 

In the context of this paper, a Python trait is simply a normal Python object attribute with some additional characteristics:

 

  • A trait has a default value which is automatically set before its first use in a program.
  • A trait is strongly typed.  Only values which meet a programmer specified set of criteria can be assigned to a trait.
  • The value of a trait can be contained either in the defining object or in another object delegated to by the trait.

 

A class can freely mix normal Python attributes with traits, or can opt to only allow the use of a fixed or open set of traits within the class. Traits defined by a class are automatically inherited by any subclass derived from the class.

 

Why would we want to use traits?

 

Python is a weakly typed language, which as any experienced Python programmer knows has both good and bad points. The main purpose of the traits package is to help address cases where weak typing leads to problems. In particular, the motivation for traits came as a direct result of work done on Chaco, an open source scientific plotting package.

 

Chaco provides a set of high-level plotting objects, each of which has a number of user settable attributes, such as line color, text font, relative location, and so on. To make the objects easy to use by scientists and engineers, the attributes attempt to accept a wide variety and style of values. For example, a color related attribute of a Chaco object might accept any of the following as legal values for the color red:

 

  • ‘red’
  • 0xFF0000
  • ( 1.0, 0.0, 0.0, 1.0 )

 

Thus, the user might write:

 

plotitem.color = ‘red’

 

In a predecessor to Chaco, providing such flexibility came at a cost:

 

  • When the value of an attribute was used by an object internally (for example, setting the correct pen color when drawing a plot line), the code would often have to map the user supplied value to a suitable internal representation, a potentially expensive operation in some cases.

 

  • If the user supplied a value outside the realm accepted by the object internals, it often caused disastrous or mysterious behavior on the part of the program. This behavior was often difficult to track down because the cause and effect were usually widely separated in terms of the logic flow of the program.

 

So one of the main goals of the traits package was to provide a form of strong type checking that would:

 

  • Allow for flexibility in the set of values an attribute could have (for example, allowing ‘red’, 0xFF0000 and ( 1.0, 0.0, 0.0, 1.0 ) as equivalent ways of expressing the color red).

 

  • Catch illegal value assignments at the point of error, and provide a meaningful and useful explanation of the error and the set of allowable values.

 

  • Eliminate the need for an object’s implementation to map user supplied attribute values into a separate internal representation.

 

In the process of meeting these design goals, the traits package evolved into a useful component in its own right, satisfying all of the above requirements and introducing a few additional, powerful features of its own.

 

Note that the traits described in this article work with any version of Python, including version 2.2 (and later) which defines a new property language feature. The Python language property feature can be used to provide the same capabilities as the traits described here, but with more work on the part of the programmer. The traits described in this document can also be layered on top of the new Python property feature to provide a more efficient implementation, although no work has yet been done to facilitate this process.

 

Now, having setting the stage with the what and why of traits, let’s roll up our sleeves and get into the good stuff…defining and using traits.

Defining Traits

To add traits to a Python class:

 

  1. Import the traits package.
  2. Derive the class from the traits module’s HasTraits class or another class already derived from HasTraits.
  3. Define the class’s instance traits by creating a class level __traits__ dictionary which describes the traits and their characteristics.

 

The following example defines a class called Person that has a single trait weight that has a default value of 0.0, and can only have floating point values:

 

import traits

 

class Person ( traits.HasTraits ):

 

   __traits__ = { ‘weight’: 0.0 }

 

It is the value associated with each __traits__ dictionary key that determines the characteristics of the trait identified by the key. Each key should be a string specifying the name of a trait defined by the class and should follow the syntax rules for a valid Python attribute name. It is also possible to specify an entire subclass of trait names with similar properties using a special type of key value we shall describe later.

 

In the example above, the key name weight specifies that the class will have a corresponding trait called weight. The value associated with weight (i.e. 0.0) specifies a very simple type of trait definition called a definition by example. The value 0.0 specifies both the default value of the trait as well as its type (i.e. a floating point number).

 

The traits module allows creating a wide variety of trait types, ranging from very simple to very sophisticated. In all cases though, traits are defined in exactly the same manner: by associating a trait definition with a trait name in a class’s __traits__ dictionary.

 

In the next section we will begin the study of trait definitions by expanding upon the simplest types of trait definitions introduced by the preceding example.

Defining Simple Traits

Perhaps the simplest type of trait is the type introduced in the preceding section: a trait defined by example. To define a trait by example, simply specify the default value the trait is to have. The default value must be one of the basic Python data types, such as:

 

  • A string
  • An integer
  • A floating point number

 

The trait will have the specified value as its default, and in addition will only allow values of the same type to be assigned to the trait.

 

Note, however, that if an assigned value is not the same type as the default value, but can be coerced to the same type, then the coerced value will be assigned to the trait. If the value cannot be coerced to the correct type, a TraitError exception will be generated.

 

For example:

 

class Person ( traits.HasTraits ):

 

   __traits__ = {

      name’: ‘’,   # string value, default is ‘’

      age’:  0,    # integer value, default is 0

      weight’: 0.0 # float value, default is 0.0

   }

 

bill = Person()

 

print bill.name, bill.age, bill.weight

# prints default values: ‘ 0 0.0’

 

bill.name   = ‘William’# OK, string

bill.name   = 5        # OK, integer coerced to string ‘5’

bill.age    = 43       # OK, integer

bill.age    = 45.7     # OK, float coerced to integer 45

bill.weight = 167.4    # OK, float

bill.weight = 172      # OK, integer coerced to 172.0

bill.weight = ‘medium’ # Error, string can’t be coerced

                       # to float

 

Another simple type of trait definition occurs when the trait can only have values which are instances of a particular class. In this case, you can provide as the trait definition:

 

  • the class
  • an instance of the class

 

If you specify a class, then any value assigned to the trait must be an instance of the specified class (or one of its subclasses), but the trait has no default value. That is, you will get a TraitError exception if you attempt to reference the trait before it has been assigned a valid class instance.

 

If you specify a class instance, then any value assigned to the trait must be an instance of the same class as the specified instance (or one of its subclasses), and the specified instance is the default value for the trait.

 

For example, continuing our previous example:

 

class Employee ( HasTraits ):

 

   __traits__ = {

      worker’:  Person,  # Must be a ‘Person’, no default

      manager’: bill     # Must be a ‘Person’,

                          # default is ‘bill’ instance

   }

 

worker_bee = Employee()

 

print worker_bee.manager

# prints out the current information for ‘bill’

 

print worker_bee.worker

# Error, no default value for the ‘worker’ trait

 

worker_bee.worker = Person( name = ‘sam’, age = 23 )

# Assigns valid value to ‘worker’ trait

 

worker_bee.manager = Employee()

# Error, value is not an instance of class ‘Person’

 

The third type of simple trait definition is to provide an exhaustive list of all possible values. The values should all be simple Python data types, such as strings, ints and floats, but do not all have to be of the same type.

 

A trait defined in this fashion can only have values that are contained in the list. The default value for the trait is the first value contained in the list.

 

For example:

 

class InventoryItem ( traits.HasTraits ):

 

   __traits__ = {

      name’:  ‘’,  # String value, default is ‘’

      stock’: [ None, 0, 1, 2, 3, ‘many’ ]

               # Enumerated list, default is ‘None’

   }

 

hats = InventoryItem()

hats.name = ‘Stetson’

 

print ‘%s: %s’ % ( hats.name, hats.stock )

# prints: ‘Stetson: None’

 

hats.stock = 2       # OK

hats.stock = ‘many’  # OK

hats.stock = 4       # Error, value not in list

 

Defining More Complex Traits

Up to this point, we have only looked at the simplest form of trait definitions. While the trait by example form is useful for many traits, there is also a more powerful and flexible type of trait that can be defined using the Trait class.

 

To create this more flexible type of trait, use an instance of a Trait class instead of a simple example in the trait definition:

 

__traits__ = { ‘trait_name’: Trait( … ) }

 

The constructor for the Trait class has several different forms:

 

Trait( class )

Trait( instance )

Trait( default_value )

Trait( default_value, other_value2, other_value3, … )

Trait([default_value, other_value2, other_value3, … ])

Trait( default_value, { python_type    |

                        constant_value |

                        dictionary     |

                        class          |

                        function       |

                        trait_handler  |

                        trait_delegate |

                        trait }+ )

Trait( trait_handler )

Trait( trait_delegate )

 

Note that in the description above, the { …|…|… }+ notation means a list of one or more of any of the items listed between the braces.

 

The Trait class constructor also accepts arbitrary keyword arguments. The value of each keyword argument gets bound to the resulting Trait object as the value of an attribute having the same name as the keyword. That is, Trait( …, foo = ‘bar’ ) will create a Trait object with a foo attribute whose value is ‘bar’. This feature allows a programmer to associate additional application specific information with a trait.

 

There are currently two keywords which are used by various trait helper classes:

 

  • desc: A string describing the intended meaning of the trait. It is used in exceptions and fly-over help in user interface trait sheets.
  • label: A string providing a human readable name for the trait. It is used to label trait values in a user interface trait sheet.

 

Although the many options for the Trait class constructor may appear daunting at first, in practice they are quite simple to use.

 

The first five forms:

 

Trait( class )

Trait( instance )

Trait( default_value )

Trait( default_value, other_value2, other_value3, … )

Trait([default_value, other_value2, other_value3, … ])

 

correspond to the trait by example cases we have already covered. The only difference is that we are using the explicit form of the Trait class to construct them. In practice, the HasTraits class automatically maps each trait by example form into the corresponding Trait object as it encounters them.

 

The next form:

 

Trait( default_value, { python_type    |

                        constant_value |

                        dictionary     |

                        class          |

                        function       |

                        trait          |

                        trait_handler  |

                        trait_delegate }+ )

 

is the most general case of a trait definition.  You specify a default value for the trait followed by a list of one or more items describing legal values for the trait (note that the default value should be included by at least one of the values in the list, otherwise an exception will occur if a program reads the trait value before it has been set).

 

The following describes each kind of value that can be included in the list in more detail:

 

python_type

Any of the following standard Python types (from the types module):

  • StringType
  • UnicodeType
  • IntType
  • LongType
  • FloatType
  • ComplexType
  • ListType
  • TupleType
  • DictType
  • FunctionType
  • MethodType
  • ClassType
  • InstanceType
  • TypeType
  • NoneType

Specifying one of these types means that the trait value must be of the corresponding Python type.

constant_value

Any constant belonging to one of the following standard Python types (from the types module):

  • NoneType
  • IntType
  • LongType
  • FloatType
  • ComplexType
  • StringType
  • UnicodeType

Specifies that the trait can have the constant as a legal value.

dictionary

A trait that includes a dictionary in the list of legal values is referred to as a mapped trait. Each dictionary key defines a legal value for the trait, while its corresponding value specifies the value the key is mapped to. Mapped traits are explained in more detail below.

class

Specifies that the trait value can be an instance of the specified class or one of its subclasses.

function

Specifies a function that will either pass or fail a value for the trait. More information about writing a trait function is provided later.

trait_handler

An instance of a TraitHandler class (or one of its subclasses). The TraitHandler class will be described in another article.

trait_delegate

An instance of a TraitDelegate class. The TraitDelegate class will be described in another article.

trait

An instance of the Trait class. Any value that is a legal value for the specified trait is also a legal value for the trait whose constructor references it.

 

Note that when more than one value from the above list is specified in the Trait constructor, any value which is acceptable to at least one of the items in the list is a valid value for the trait. For example:

 

import types

 

class Nonsense ( traits.HasTraits ):

 

   __traits__ = {

      rubbish’: traits.Trait( 0.0, 0.0,

                               stuff’, types.TupleType )

   }

 

The Nonsense class has a rubbish trait which has a default value of 0.0 and can have any of the following three values:

 

  • The float value 0.0
  • The string value ‘stuff’
  • Any Python tuple

 

Note that in this case it was necessary to specify 0.0 twice: the first occurrence defines the default value, and the second occurrence specifies 0.0 as one of the trait’s legal values.

 

The following shows what would happen if we left the second 0.0 out of the Trait constructor:

 

import types

 

class Nonsense ( traits.HasTraits ):

 

   __traits__ = {

      rubbish’: traits.Trait( 0.0, ‘stuff’,

                               types.TupleType )

   }

 

foo  = Nonsense()

oops = foo.rubbish # Error, generates following exception:

 

Traceback (most recent call last):

  File "<stdin>", line 1, in ?

  File "traits.py", line 1073, in __getattr__

    raise TraitError, '%s %s' % ( str( excp )[:-1],

traits.TraitError: The 'rubbish' trait of a Nonsense instance must be of type 'tuple' or 'stuff', but a value of 0.0 was specified as the default value. The

trait must be assigned a valid value before being used.

 

foo.rubbish = ( 1, 2, 3 )  # OK, legal value

 

Notice also the descriptive text generated by the above exception. One of the side benefits of using the traits mechanism is the ability to automatically generate detailed exceptions when a program incorrectly uses or sets a trait.

 

We can further improve the content of a generated exception by using the desc keyword when defining a trait:

 

import types

 

class Nonsense ( HasTraits ):

 

   __traits__ = {

      rubbish’: traits.Trait( 0.0, ‘stuff’,

                               types.TupleType,

                               desc = ‘total rubbish’ )

   }

 

foo  = Nonsense()

foo.rubbish = 1 # Error, generates following exception:

 

Traceback (most recent call last):

  File "<stdin>", line 1, in ?

  File "traits.py", line 1090, in __setattr__

    raise TraitError, excp

traits.TraitError: The 'rubbish' trait of a Nonsense instance specifies total rubbish and must be of type 'tuple' or 'stuff', but a value of 1 was specified.

Mapped Traits

If a Trait constructor contains one or more dictionaries, then the resulting trait is called a mapped trait. In practice this means that the resulting object actually contains two attributes: one containing one of the dictionary keys representing the current value of the trait, and the other containing its corresponding value (i.e. the mapped value). The named of the mapped attribute is simply the base trait name with an underscore appended to the end.

 

The following illustrates a boolean trait defined as a mapped trait:

 

true_boolean = Trait( ‘true’, { ‘true’:  1,

                                t’:     1,

                                yes’:   1,

                                y’:     1,

                                1:       1,

                                false’: 0,

                                f’:     0,

                                no’:    0,

                                n’:     0,

                                0:       0 } )

false_boolean = Trait( ‘false’, true_boolean )

                                  

class Kid ( HasTraits ):

   __traits__ = {

      likes_ice_cream’: true_boolean,

      likes_spiders’:   false_boolean,

   }

 

The Kid class has two traits: likes_ice_cream and likes_spiders. Because the true_boolean trait uses a dictionary, both likes_ice_cream and likes_spiders are mapped traits, which means that each Kid instance also has two mapped attributes: likes_ice_cream_ and likes_spiders_. Any time a new value is assigned to either likes_ice_cream or likes_spiders, the corresponding mapped attribute is updated with the value in the dictionary corresponding to the value assigned.

 

For example:

 

sally = Kid()

 

print sally.likes_ice_cream, sally.likes_spiders

# prints: true false

 

print sally.likes_ice_cream_, sally.likes_spiders_

# prints: 1 0

 

mikey = Kid()

mikey.likes_ice_cream = ‘no’

mikey.likes_spiders   = ‘y’

 

print mikey.likes_ice_cream, mikey.likes_spiders

# prints: no y

 

print mikey.likes_ice_cream_, mikey.likes_spiders_

# prints: 0 1

 

This example illustrates how a mapped trait can be used to create a programmer friendly attribute (e.g. likes_ice_cream) and a corresponding program friendly mapped attribute (i.e. likes_ice_cream_). The mapped attribute is program friendly because it is usually in a form that can be directly used by program logic (in this case a boolean value).

 

Another point illustrated by this example is the ability to create new traits from existing ones. In this case, the false_boolean trait is created by re-using the true_boolean trait and specifying a new default value. This is often an efficient way of reusing an existing trait because it shares most of the internal state of the existing trait without having to make a completely new copy of the trait description.

 

There are a couple of other points to keep in mind when creating a mapped trait:

 

  • If not all values in the Trait constructor are dictionaries, the non-dictionary values are copied directly to the mapped attribute (i.e. the mapping used is the identity mapping).

 

  • If only dictionaries are used in the Trait constructor and the composite mapping defined by the dictionaries is 1:1, the trait is a reversible mapped trait. This means that any value assigned to the mapped attribute (i.e. the attribute with the underscore appended) is reverse mapped back into the base trait’s corresponding value.

 

If the composite mapping is not 1:1, assigning to the mapped attribute has no effect on the base trait. In this case it is not recommended to assign values to the mapped attribute, since it is possible for the base trait and its mapped attribute to become inconsistent.

 

The first case is illustrated below:

 

class Balance ( HasTraits ):

   __traits__ = {

      sign’: Trait( ‘positive’, -1, 0, 1,

                     { ‘positive’:  1,

                       ‘negative’: -1 } )

 

In this example, the Balance class has a sign trait which has a default value of ‘positive’, and which can have -1, 0, 1, ‘positive’ and ‘negative’ as legal values. Its corresponding mapped attribute, sign_, can only have the values: 1, 0, -1. Assigning 1, 0 or -1 to the sign trait assigns the same value to the sign_ attribute. Assigning ‘positive’ or ‘negative’ to sign assigns the corresponding 1 or -1 value to sign_.

 

To illustrate the second case, we can refer back to our previous example using the true_boolean and false_boolean traits. As originally defined, these traits are not reverse mapped traits because the dictionary mapping is not 1:1 (i.e. several dictionary keys map to the value 1, and several other keys map to the value 0).

 

However, if we modify the true_boolean trait definition, we can make the likes_ice_cream and likes_spiders traits reverse mapped:

 

true_boolean = Trait( ‘yes’, { ‘yes’: 1,

                               ‘no’:  0 } )

 

Because this mapping is 1:1, the likes_ice_cream and likes_spiders traits are now reverse mapped traits:

 

mikey = Kid()

mikey.likes_ice_cream = ‘no’

mikey.likes_spiders_  = 1

 

print mikey.likes_ice_cream, mikey.likes_spiders

# prints: no yes

 

print mikey.likes_ice_cream_, mikey.likes_spiders_

# prints: 0 1

 

Trait Functions

It is also possible to specify legal values for a trait by providing a function reference in the Trait constructor. A function used in this way must have the following prototype:

 

function ( object, name, value )

 

where:

 

  • object: the object whose trait is being assigned to.
  • name: the name of the object trait being assigned to.
  • value: the value being assigned to the object attribute.

 

The function is invoked whenever a value is assigned to the trait. Normally the function does not need to know the object or trait name being assigned to, but they are provided in case the testing performed by the function is context dependent.

 

The function indicates a value is valid by returning normally. The value returned by the function is used as the value of the object trait. That is, the function can return the original value passed to it or any other value, perhaps derived from the original value. In any case, the value returned is the value assigned to the object trait.

 

The function indicates that a value is not valid by throwing an exception. The type of exception thrown is immaterial because it is always caught by the trait mechanism and mapped into a TraitError exception.

 

To illustrate:

 

from types import StringType

 

def bounded_string ( object, name, value ):

    if type( value ) != StringType:

       raise TypeError

    if len( value ) < 50:

       return value

    return ‘%s…%s’ % ( value[:24], value[-23:] )

 

The bounded_string function can be used in a Trait constructor to define a trait whose value must be a string, and whose value will never exceed 50 characters in length. Long strings are shortened to 50 characters by removing excess characters from the middle of the string.

 

In order to allow the exceptions generated by traits based on functions to be as descriptive as possible, you can attach a short string describing the values accepted by the function as the info trait of the function.

 

For example, continuing our bounded_string example:

 

bounded_string.info = ‘a string no longer than 50 characters’

 

The string contained in the function’s info attribute will be merged with other information about the trait whenever an exception occurs assigning to the trait. If the info attribute is not defined, the string ‘a legal value’ will be used in its place.

 

Putting this all together:

 

class DataBaseRecord ( HasTraits ):

   __traits__ = {

      part_desc’: Trait( None, None, bounded_string )

   }

 

sprocket = DataBaseRecord()

sprocket.part_desc = 0 # Generates this exception:

 

Traceback (most recent call last):

  File "<stdin>", line 1, in ?

  File "traits.py", line 1090, in __setattr__

    raise TraitError, excp

traits.TraitError: The 'part_desc' trait of a DataBaseRecord instance must be a string no longer than 50 characters or None, but a value of 0 was specified.

 

This illustrates how a function can be combined with other values in a Trait constructor to create a composite trait and how the function’s info attribute is used when generating a TraitError exception.

Digging deeper

Up to this point, we’ve introduced the notion of traits as strongly typed Python object attributes, and showed how powerful a tool they can be, as well as how simple they can be to define and use. We’ll new continue our exploration of traits by digging deeper into more sophisticated methods of defining traits.

 

In essence, we’ll be taking traits far beyond the basics covered up till now, covering the full range of capabilities exposed by the traits package. So, having already rolled up our sleeves, it’s time to really get our hands dirty by digging into the inner workings of what makes traits tick.

Trait Handlers

As stated previously, a trait is defined by associating a trait definition with a trait name in a class’s __trait__ dictionary:

 

import traits

 

class a_class ( traits.HasTraits ):

 

   __traits__ = { trait_name: trait_definition }

 

Furthermore, although a trait definition can have several forms, the most general case is given by specifying a Trait class instance whose constructor has the form (among others) of:

 

Trait( default_value, { python_type    |

                        constant_value |

                        dictionary     |

                        class          |

                        function       |

                        trait_handler  |

                        trait_delegate |

                        trait }+ )

 

Previously, we deferred describing what a trait_handler or trait_delegate is. But now the time has come to address that omission, starting with what a trait handler is.

TheTraitHandler Class

A trait handler is an instance of the TraitHandler class, whose task is to verify the correctness of values assigned to object traits. In essence, they are very similar to the trait functions we described previously, but with several advantages over functions:

 

  • Because they are objects, they can have constructors and state. This allows the creation of parameterized types, some concrete examples of which will be given shortly.

 

  • Because they are class based, they can have multiple methods, as opposed to functions, which have only a single callable interface. This allows more flexibility in their implementation and allows them to handle a wider range of cases, such as interactions with other components, like the trait sheet user interface mechanism we will describe later.

 

Right out of the box, the traits package comes with a number of predefined TraitHandler subclasses which handle a wide variety of trait definitions. In fact, all of the trait definitions described so far ultimately rely on one or more of the TraitHandler subclasses described in the next few sections.

The TraitType Class

Let’s start our discussion of the predefined TraitHandler subclasses with a class which, under the covers, has been used extensively in all of the examples up to this point:

TraitType. An instance of TraitType basically ensures that a value assigned to a trait is of a specified Python type. Its constructor is of the form:

 

TraitType( a_type )

 

where a_type is either a Python type (e.g. types.StringType) or a Python value (e.g. ‘cat’). In the latter case, the Python value is mapped to its corresponding Python type. For example, the string ‘cat’ would be automatically mapped to types.StringType.

 

Any trait which uses a TraitType instance in its definition will ensure that its value is of the same type associated with the TraitType instance. This is an example of a parameterized type, since the single TraitType class allows creating instances which check for totally different sets of values.

 

For example:

 

class Person ( HasTraits ):

 

   __traits__ = {

      name’:   Trait( ‘’,  TraitType( ‘’ ) ),

      weight’: Trait( 0.0, TraitType( types.FloatType ) )

   }

 

In this example, the name trait must be of type string, while the weight trait must be of type float, although both are based on instances of the TraitType class. Note that this example is essentially the same as writing:

 

class Person ( traits.HasTraits ):

 

   __traits__ = {

      name’:   ‘’,

      weight’: 0.0

   }

 

This simpler form is automatically changed by the HasTraits class into the first form, based on TraitType instances, as the traits are defined.

 

One additional point about the TraitType class is that if the ultimate type of the argument supplied to the constructor is one of the following Python types:

 

  • IntType
  • LongType
  • FloatType
  • ComplexType
  • StringType
  • UnicodeType

 

the instance automatically attempts to coerce any trait value to its corresponding type. For example, a TraitType(0) instance attempts to coerce any assigned trait value to an integer using the standard Python int function.

 

Each type in the list uses its corresponding standard Python casting function to perform the coercion:

 

·       int for IntType

·       long for LongType

·       float for FloatType

·       complex for ComplexType

·       unicode for UnicodeType

 

The only exception is StringType. Since nearly every Python value, including arbitrary Python objects, can be coerced to a string using the Python str function, only values of type StringType, UnicodeType, IntType, LongType, FloatType or ComplexType are coerced using the standard Python str function. All other values are explicitly disallowed.

The TraitInstance Class

Closely related to TraitType is the TraitInstance class, which checks to ensure that trait values belong to a specified Python class. The constructor for the TraitInstance class has the form:

 

TraitInstance ( a_class )

 

where a_class is either a Python class, or an instance of a Python class. If it is an instance of a class, it is mapped to the class it is an instance of.

 

Any trait which uses a TraitInstance ensures that its values belong to the specified class (or one of its subclasses). For example:

 

class Employee ( HasTraits ):

 

   __traits__ = {

      manager’: Trait( None, None,

                        TraitInstance( Employee ) )

     

   }

 

defines a class Employee which has a manager trait which accepts either None or an instance of Employee as its value.

The TraitFunction Class

The TraitFunction class ensures that assigned trait values are acceptable to a specified validator function. The constructor for a TraitFunction instance has the form:

 

TraitFunction ( a_function )

 

where a_function is the function that will validate whether a particular trait value is valid or not. Note that it is to this type of TraitHandler that the trait functions described in the first article are automatically mapped by the HasTraits class when they are encountered in a trait definition.

 

As with a trait function, the signature of the function specified in the TraitFunction constructor must be of the form:

 

function ( object, name, value )

 

The function must verify that value is a legal value for the name trait of the specified object. If it is, the value returned by the function is the actual value assigned to the trait. If it is not, the function must generate an exception. The actual type of the exception does not matter, because the TraitFunction instance handles all exceptions generated by the function and maps them into TraitError exceptions.

 

For example, we previously presented a trait defined using a trait function:

 

from types import StringType

 

def bounded_string ( object, name, value ):

    if type( value ) != StringType:

       raise TypeError

    if len( value ) < 50:

       return value

    return ‘%s…%s’ % ( value[:24], value[-23:] )

 

class DataBaseRecord ( HasTraits ):

   __traits__ = {

      part_desc’: Trait( None, None, bounded_string )

   }

 

This could also be expressed using the following DataBaseRecord class definition:

 

class DataBaseRecord ( HasTraits ):

   __traits__ = {

      part_desc’: Trait( None, None,

                       TraitFunction( bounded_string ) )

   }

 

As with the preceding examples, the HasTraits class automatically maps the first definition of the part_desc trait into its second, equivalent, form.

The TraitRange Class

An instance of the TraitRange class ensures that a trait value lies within a specified numeric range. The constructor for the TraitRange class has the form:

 

TraitRange ( low, high )

 

where low and high are the minimum and maximum values allowed for the trait’s value. Both low and high should be of the same Python numeric type, which can be any one of the following:

 

  • IntType
  • LongType
  • FloatType
  • ComplexType

 

Besides checking that a trait value lies within the specified numeric range, a TraitRange instance automatically attempts to coerce a trait value to the same type specified by its low and high values using the appropriate standard Python int, long, float or complex function.

 

For example:

 

class Person ( HasTraits ):

 

   __traits__ = {

      age’:    Trait( 0,   TraitRange( 0, 150 ) ),

      weight’: Trait( 0.0, TraitRange( 0.0, 800.0 ) )

   }

 

defines a Person class which has an age trait which must be an integer in the range from 0 to 150, and a weight trait which must be a float value in the range from 0.0 to 800.0.

The TraitEnum Class

Instances of the TraitEnum class verify that a trait’s value is a member of a specified legal, list of values. The constructor for a TraitEnum instance has the form:

 

TraitEnum ( legal_values )

 

where legal_values is a list or tuple enumerating all legal values for the trait. Note that the list can also be provided as a list of values to the constructor.

 

For example:

 

class Flower ( HasTraits ):

 

   __trait__ = {

      color’: Trait( ‘white’, TraitEnum( [

                      white’, ‘yellow’, ‘red’ ] ) )

      kind’:  Trait( ‘annual’, TraitEnum(

                      annual’, ‘perennial’ ) )

   }

 

defines a Flower class which has a color trait which can have one of the three strings ‘white’, ‘yellow’ or ‘red’ as its value. As with several preceding examples, this is equivalent to the following class definition:

 

class Flower ( HasTraits ):

 

   __trait__ = {

      color’: [ ‘white’, ‘yellow’, ‘red’ ]

      kind’:  ( ‘annual’, ‘perennial’ )

   }

 

 

The HasTraits class automatically maps the second form to the first whenever it encounters it in a trait definition.

The TraitPrefixList Class

The TraitPrefixList class is a variation on the TraitEnum class. Its constructor has the form:

 

TraitPrefixList ( legal_value_strings )

 

where legal_value_strings is a list or tuple of strings. Alternatively, the set of strings can also be enumerated directly in the constructor argument list.

 

The values that can be assigned to a trait defined using a TraitPrefixList is the set of all strings supplied to the TraitPrefixList constructor as well as any unique prefix. That is, if the set of strings supplied to the TraitPrefixList constructor is described by [ s1, s2, …, sn ], then the string v is a valid value for the trait if:

 

v == si[:j] for one and only one pair of values (i,j).

 

If v is a valid value, then the actual value assigned to the trait is the corresponding si that v matched. For example:

 

class Person ( traits.HasTraits )

   __traits__ = {

      married’: Trait( ‘no’, TraitPrefixList(

                        yes’, ‘no’ ) )
   }

 

The Person class has a married trait which will accept any of the strings: ‘y’, ‘ye’, ‘yes’, ‘n’, or ‘no’ as valid values. The actual values assigned as the value of the trait however is limited to ‘yes’ or ‘no’. That is, if the value ‘y’ is assigned to the married trait, the actual value stored will be ‘yes’.

 

As another example, consider:

 

class Alien ( traits.HasTraits )

   __traits__ = {

      heads’: Trait( ‘one’, TraitPrefixList(

                      [ ‘one’, ‘two’, ‘three’ ] ) )
   }

 

In this case, it is valid to assign either ‘tw’ (i.e. ‘two’) or ‘th’ (i.e. ‘three’) as the value of the heads trait, but not ‘t’, since ‘t’ is not a unique prefix and so it is ambiguous which of the root values, ‘two’ or ‘three’, is meant.

 

Note that the algorithm used by the TraitPrefixList in determining if a string is a valid value is fairly efficient in terms of both time and space, and is not based on a brute force set of comparisons.

The TraitMap Class

The TraitMap class implements the mapped traits described earlier. The constructor for TraitMap instances has the form:

 

TraitMap ( mapping )

 

where mapping is a dictionary whose keys are the valid values for the trait, and whose corresponding values are the values the keys are mapped into. Refer back to the earlier section for a more detailed discussion of mapped traits.

 

For example, we previously defined the following trait:

 

true_boolean = Trait( ‘yes’, { ‘yes’: 1,

                               ‘no’:  0 } )

 

This definition is equivalent to the following:

 

true_boolean = Trait( ‘yes’, TraitMap( { ‘yes’: 1,

                                         ‘no’:  0 } ) )

 

The Trait class automatically converts the first form to the second internally.

The TraitPrefixMap Class

The TraitPrefixMap is sort of a cross between the TraitPrefixList and TraitMap classes. Its constructor has the form:

 

TraitPrefixMap ( mapping )

 

where mapping is a dictionary similar to TraitMap. The one difference is that each key in mapping must be a string (which is not a requirement for TraitMap instances). As with TraitPrefixList instances, a string v is a valid value for the trait if it is a prefix of one, and only one, key k in mapping. The actual value assigned to the trait is k, and its corresponding mapped attribute is assigned mapping[k].

 

Earlier, we created the following definition for a boolean trait:

 

true_boolean = Trait( ‘true’, { ‘true’:  1,

                                t’:     1,

                                yes’:   1,

                                y’:     1,

                                1:       1,

                                false’: 0,

                                f’:     0,

                                no’:    0,

                                n’:     0,

                                0:       0 } )

 

We can create a similar definition, which actually allows more freedom in the set of acceptable values, using a TraitPrefixMap:

 

true_boolean = Trait( ‘true’, TraitPrefixMap( {

                                true’:  1,

                                yes’:   1,

                                false’: 0,

                                ‘no’:    0 } ),

                             { 1: 1, 0: 0 } )

 

This definition allows any prefix of ‘true’, ‘yes’, ‘false’, or ‘no’ to be assigned as a valid value, as well as the integer values 0 and 1. As in the previous example, the mapped attribute for the trait will only have 0 or 1 as its value.

 

Note also the use a simple dictionary (converted to a TraitMap internally by the Trait class) to handle the set of legal, non-string mapped values (i.e. 0 and 1).

The TraitComplex Class

The TraitComplex class provides a logical or combination of other trait handlers. Its constructor has the form:

 

TraitComplex ( handlers )

 

where handlers is a list or tuple of TraitHandler or Trait objects. Alternatively, all of the TraitHandler or Trait objects can be provided directly as arguments to the constructor.

 

A value is a valid value for a trait based on a TraitComplex instance if it is a valid value for at least one of the TraitHandler or Trait objects supplied to the constructor. In addition, if at least one of the TraitHandler or Trait objects is mapped (e.g. based on a TraitMap or TraitPrefixMap instance), then the TraitComplex instance is also mapped. In this case, any non-mapped Traits or TraitHandlers will use the identity mapping.

 

The TraitComplex class provides a means of creating more complex trait definitions by combining several simpler trait definitions, and has been used extensively in many of the examples in this and the preceding article, although its use has been implicit until now.

 

For instance, the preceding example can be rewritten as:

 

true_boolean = Trait( ‘true’,

                      TraitComplex(                         

                         TraitPrefixMap( {

                            true’:  1,

                            yes’:   1,

                            false’: 0,

                            ‘no’:    0 } ),

                         TraitMap( { 1: 1, 0: 0 } ) ) )

 

In fact, this is exactly the rewriting internally performed by the Trait class when it encounters the original example.

Defining Your Own 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, needed to completely specify your trait. The constructor does not need to call the TraitHandler base class’s constructor.

 

The only method that your trait handler must implement is:

 

setattr ( self, object, name, value, default )

 

which is called whenever a new value is assigned to a trait defined using your trait handler. The meanings of the parameters passed to the setattr method are:

 

  • object: The object whose trait is being assigned.
  • name: The name of the trait being assigned.
  • value: The proposed new value for the trait.
  • default: The default value associated with the trait.

 

Your method should verify that the new value being assigned is valid. If it is, your setattr method should call the traits module’s setvalue function to do the actual assignment of the trait value. The signature for setvalue is:

 

setvalue ( object, name, value, default )

 

where object, name, value and default have the same meanings as the parameters passed to the setattr method. Note however, that the value specified to setvalue not need be the same value passed to your settattr method. For example, some TraitHandler subclasses coerce their input values to a type acceptable to the trait.

 

If the value received is not valid for the trait, your setattr method should call the error method:

 

error ( self, object, name, value )

 

where object, name and value are the same values passed to your setattr method. The error method will raise an appropriate exception to either notify the user of the problem or, in the case of complex traits, allow another trait handler a chance to validate the value.

 

In many cases, it will also be necessary to define one or more of the following methods in your TraitHandler subclass:

 

info ( self )

 

is_mapped ( self )

             

get_editor ( self, trait )

 

The info method should return a string describing the type of value accepted by your trait handler. It should be a phrase describing the type rather than a complete sentence (e.g. “a square sprocket” instead of “The value must be a square sprocket.”). The value returned by info will be combined with other information whenever an error occurs and will make more sense to the user if the result is a phrase.

 

Note that the result may include information specific to the particular trait handler instance. For example, TraitRange instances return a string indicating the range of values acceptable to the handler (e.g. “an integer in the range from 1 to 9”).

 

If the info method is not overridden, the default method returns the string “a legal value”.

 

The is_mapped method only needs to be overridden if your trait maps its values to a shadow trait, like the TraitMap trait handlers. In this case, your is_mapped method should return 1 to indicate that your trait is mapped. Alternatively, you can subclass from TraitMap, whose is_mapped method already returns 1.

 

The get_editor method only needs to be specified if traits defined using your trait handler require a non-default trait editor in interactive traits sheets created using the traits package’s trait sheet support. This topic will be addressed more completely in the next article. The default get_editor method returns a trait editor that allows the user to type in an arbitrary string as the value for the trait.

 

To illustrate all of the above, the following is a definition of a trait handler that only allows positive, odd integers as legal values:

 

import traits, types

 

class TraitOddInteger ( TraitHandler ):

 

   def setattr ( self, object, name, value, default ):

       if ((type( value ) == types.IntType) and

           (value > 0) and ((value % 2) == 1)):

          setvalue( object, name, value, default )

       else:

          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:

 

class AnOddClass ( HasTraits ):

 

   __traits__ = {

      oddball:   Trait(  1, TraitOddInteger() ),

      very_odd’: Trait( -1, TraitOddInteger(),

                             TraitRange( -10, -1 ) )

   }

 

The reason the info method returns a phrase rather than a complete sentence is illustrated by the following erroneous use of the very_odd trait:

 

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 __setattr__

    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 highlighted result returned by the info method embedded in the exception generated by the invalid assignment.

Trait delegation

One of interesting capabilities of the traits package is its ability to delegate the definition and default value of a trait to another object. This has use in 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. Traits based on delegation are defined using instances of the TraitDelegate class.

The TraitDelegate class

The constructor for a TraitDelegate has the form:

 

TraitDelegate( delegate, mutate_or_prefix = 0 )

 

The delegate parameter is a string that specifies the name of the object attribute used to derive a trait’s delegate. The attribute can specify either a method or a value. The difference between the two types is explained in the next two sections.

 

The mutate_or_prefix parameter plays several roles, which will also be explained in subsequent sections. It can either be a string, a boolean value (i.e. zero or non-zero), or omitted.

Delegating Via a Method

If the delegate parameter to a TraitDelegate constructor specifies a method of the object, then the method will be invoked each time the trait delegate is needed. The method is called without any arguments, and must return the object to use as the trait’s delegate. This style of delegation is useful in cases where the delegate may depend on object state or other factors that might change over time.

Delegating Via Other Traits or Attributes

If the delegate parameter to a TraitDelegate constructor specifies a trait or attribute of the object other than a method, then the current value of the trait or attribute is used as the delegate whenever the delegate is needed. In practice, this is more commonly used than specifying a delegate using an object method, since it simpler to set up and does not require writing any code.

The Different Types of Delegation

The delegate parameter to the TraitDelegate constructor specifies how to determine a trait’s delegate. The mutate_or_prefix parameter to the constructor specifies additional information about how to do the delegation.

 

If the mutate_or_prefix parameter is omitted, then delegation is to an attribute of the delegate object with the same name as the trait. Consider the following example:

 

class Child ( HasTraits ):

 

   __traits__ = {

      first_name’: ‘’,

      last_name’:  TraitDelegate( ‘father’ ),

      father’:     Person,

      mother’:     Person

   }

 

class Parent ( HasTraits ):

 

   __traits__ = {

      first_name’: ‘’,

      last_name’:  ‘’

   }

 

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

printsJones

 

sally.last_name = sally.mother.last_name

print sally.last_name

printsSmith

 

sally.last_name = sally.mother # Error: string expected

 

A Child object delegates its last_name trait to its father attribute’s last_name trait. Because the mutate_or_prefix parameter was not specified in the TraitDelegate constructor used to define the last_name trait, the trait name of the delegate is the same as the original trait name. Thus, by default, the last_name of a Child is the same as the last_name of its father.

 

Note however that once we explicitly assign a value to the last_name trait of a Child, it takes on the assigned value, as illustrated in the example when we explicitly set Sally’s last name 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 trait, as illustrated in the example when we attempted to assign Sally’s mother as her last name. The last_name trait delegates the assignment to the father trait, whose last_name trait specifies that the only legal values are strings.

 

When the mutate_or_prefix parameter to a TraitDelegate constructor is a string, the rule we just described for performing trait look-up in the delegated to object is modified, with the modification depending on the format of the mutate_or_prefix string:

 

  • If mutate_or_prefix is a valid Python attribute name, then the original trait name is replaced by mutate_or_prefix when performing the delegate object trait look-up.

 

  • If mutate_or_prefix ends with an asterisk (‘*”), and is longer than one character, then mutate_or_prefix, minus the trailing asterisk, is added to the front of the original trait name when performing the delegate object trait look-up.

 

  • If mutate_or_prefix is equal to a single asterisk (‘*’), the value of the object class’s __prefix__ attribute is added to the front of the original trait name when performing the delegate object trait look-up.

 

Each of these three possibilities is illustrated in the following example:

 

class Child ( HasTraits ):

 

   __prefix__ = ‘child_’

 

   __traits__ = {

      first_name’: TraitDelegate( ‘mother’,

                                   ‘favorite_*’ ),

      last_name’:  TraitDelegate( ‘father’,

                                   family_name’ ),

      allowance’:  TraitDelegate( ‘father’, ‘*’ ),

      father’:     Person,

      mother’:     Person

   }

 

class Parent ( HasTraits ):

 

   __traits__ = {

      first_name’:          ‘’,

      family_name’:         ‘’,

      favorite_first_name’: ‘’,

      child_allowance’:     1.00

   }

 

In this example, instances of the Child class have three delegated traits:

 

  • first_name, which delegates to the favorite_first_name trait of its mother trait.
  • last_name, which delegates to the family_name trait of its father trait.
  • allowance, which delegates to the child_allowance trait of its father trait.

 

The final form of delegation occurs when the mutate_or_prefix argument to the TraitDelegate constructor is not a string and non-zero (e.g. 1). In this case, the trait delegates to the trait specified by the delegate argument to the constructor as before, but any changes to the trait are made to the delegate object’s trait value, not to the object delegating the trait.

Reusing Trait Definitions

All of the traits defined so far have applied to a single attribute of a class. Because a Trait object only describes the characteristics of a trait, and not the current value of a trait, it can be used in the definition of any number of traits. For example:

 

class quadratic ( HasTraits ):

 

   coefficient = Trait( 0.0, TraitRange( -1.0, 1.0 ) )

 

   __traits__ = {

      ‘c2’: coefficient,

      ‘c1’: coefficient,

      ‘c0’: coefficient,

      x’:  Trait( 0.0, TraitRange( -100.0, 100.0 ) )

   }

 

In this example the traits c2, c1 and c0 are defined in terms of a common coefficient trait.

Special Predefined Traits and Values

In addition to the tools provided for defining your own application specific traits, the traits package also includes a few special predefined traits:

 

  • DefaultPythonTrait: Specifies that an attribute should have normal Python attribute behavior. That is, it should allow any value to be assigned to it, and should generate an exception if a program attempts to read its value before it has been assigned. This trait is often used in conjunction with the wildcard naming rule described in the next section to provide standard Python behavior to an entire category of object attributes.

 

  • AnyValue: Specifies that an attribute allows any value to be assigned and has an initial, default value of None. This is different than a normal Python attribute since it will return None if referenced before assignment rather than generate an exception.

 

  • Disallow: Specifies that the attribute can not be read or set. This trait is most often used in conjunction with the wildcard naming rule explained in the next section, and can be used, for example, to catch attribute spelling mistakes made by users.

 

  • ReadOnly: Specifies that an attribute is read-only. In practice, the attribute is actually write-once. The attribute allows any value to be written to it initially, but after the first assignment, any attempted re-assignment causes an exception. As a result, traits of this type are normally initialized in the object constructor. Typical uses include constants and values such as lists, which can be modified, but not re-assigned.

 

The following is an example using most of these special traits:

 

class Person ( traits.HasTraits ):

 

   __traits__ = {

      children’:       traits.ReadOnly,

      favorite_thing’: traits.AnyValue,

      ‘*’:              traits.Disallow # See next section

   }

 

   def __init__ ( self ):

       self.children = []  # Initialize ‘read-only’ value

 

me = Person()

me.children.append(Mikey’ )  # OK

me.children = ‘no way’         # Illegal, ‘read-only’

me.favorite_thing = ‘fudge’    # OK

me.favoritething  = ‘fudge’    # Illegal, misspelled and

                               # caught by ‘Disallow’ rule

 

Creating Categories of Similar Traits for Objects

Up until now, all trait definitions have bound a single object attribute to a specified trait definition. However, there are cases where it might be useful to associate an entire category of object attributes with a particular trait definition. The traits package allows you to do this by including a wildcard character (i.e. ‘*’) at the end of the trait name.

 

For example:

 

class Person ( HasTraits ):

 

   __traits__ = {

      ‘temp_*’: AnyValue

   }

 

defines a class Person with an entire category of attributes with names beginning with ‘temp_’  that have the AnyValue trait (i.e.  they have None as their default value and allow any value to be assigned to them). Thus anyone using a Person instance can reference attributes temp_count, temp_name or temp_whatever without having explicitly declared them in the traits dictionary and each attribute will have None assigned as the default value and allow assignment of any value.

 

It is even possible to give all object attributes a default trait by specifying only the wildcard character in the trait definition:

 

class Person ( traits.HasTraits ):

 

   __traits__ = {

      ‘*’: traits.AnyValue

   }

 

In this case, all Person instance attributes will have the AnyValue trait.

 

When using wildcard characters in trait names, the following two rules are used to determine what trait an attribute has:

 

  1. If an attribute name exactly matches a name without a wildcard character in the traits dictionary, that definition applies.
  2. If an attribute name matches several names with wildcard characters in the traits dictionary, the definition with the longest name applies.

 

These rules are illustrated in the following class definition:

 

class Person ( traits.HasTraits ):

   __traits__ = {

      temp_count’: -1,

      ‘temp_*’:     traits.AnyValue

      ‘*’:          traits.DefaultPythonTrait

   }

 

In this case, the Person class has a temp_count trait which must be an integer and has a default value of -1. Any other attribute name starting with ‘temp_’ will have a default value of None and allow any value to be assigned. All other object attributes will behave like normal Python attributes (i.e. they will allow any value to be assigned, but must have a value assigned to them before their first reference).

 

One final example is:

 

class Person ( traits.HasTraits ):

   __traits__ = {

      name’:   ‘’,

      age:     0,

      weight’: 0.0,

      ‘*’:      traits.Disallow

   }

 

In this example, a Person instance has three traits:

 

  • name (must be a string, default is ‘’)
  • age (must be an integer, default is 0)
  • weight (must be a float, default 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 will cause an exception.

Per object traits

The traits package also allows you to define objects with traits that are specific to a particular object, rather than class. This is accomplished by using, or subclassing from, the HasDynamicTraits class, rather than the HasTraits class.

 

For example:

 

rock = HasDynamicTraits()

rock.add_trait( ‘density’, 1.2 )

 

creates an object called rock that has a trait called density, which has an initial, default value of 1.2 and only accepts floating point values. Note that although the object only has a single trait defined, the object can still have normal Python attributes defined on it, such as:

 

rock.weight = 5.4

 

If we wanted to further restrict the object so that only the density trait is available, we could add the following trait:

 

rock.add_trait( ‘*’, Disallow )

 

which explicitly prevents access to undefined traits or attributes.

Useful Methods on Objects with Traits

Creating a subclass of HasTraits gives any Python class the ability to have traits defined on it. But in addition, it also provides a number of useful traits related methods. The following is a brief description of some of the methods available:

 

 set ( self, **traits )

Treats each keyword argument to the method as a trait name and sets the corresponding trait to the value specified. This is a useful shorthand when a number of traits need to be set on an object, or a trait value needs to be set in a lambda function. For example, you could write person.set( name = ‘Bill’, age = 27 ) instead of person.name = ‘Bill’; person.age = 27.

 

add_trait ( self, name, trait )

Adds the trait specified by trait as a new trait called name. If name ends with a ‘*’, then it defines a new trait class as described previously. If the object’s class derives from HasTraits, then the trait applies to all instances of the class. If the object’s class derives from HasDynamicTraits then the trait only applies to the object the method was invoked on.

 

on_trait_change ( self, handler, name = None, remove = 0 )

Invokes the specified handler whenever the object trait called name is modified. If remove is not 0, then handler will no longer be invoked when the trait is modified (i.e. the handler is removed). Multiple handlers can be defined on the same object, or even for the same trait on the object. If name is not specified or None, the handler is invoked when any trait on the object is changed.

          

reset_traits ( self, *names )

Resets each of the traits whose names are specified in the argument list to their default value. If no trait names are specified, it resets all object traits to their default values.

 

clone_traits ( self, other )

Assigns the corresponding trait value from the other object to each trait defined on the object.

Performance Considerations of Traits

Using traits can impose a performance penalty on attribute access over and above that of normal Python attributes. The extent of the penalty can depend on several factors, perhaps the two most important of which are:

 

·       Whether the trait delegates or not.

·       The complexity of the trait definition.

 

If a trait does not delegate, the performance penalty can be characterized as follows:

 

  • Getting: No penalty (i.e. standard Python attribute access speed).
  • Setting: Depends upon the complexity of the validation tests performed by the trait definition.

 

If a trait does delegate, the cases to be considered are:

 

  • Getting the default value: Cost of following the delegation chain.
  • Getting an explicitly assigned value: No penalty (i.e. standard Python attribute access speed).
  • Setting: Cost of following the delegation chain plus the cost of performing the validation of the new value.

 

Note that in the case where delegation modifies the delegate object, the cost of getting an attribute is always 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, since the only impact 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 constantly following delegation chains may impose a significant performance impact on the application. Of course, this must be offset be the convenience and flexibility provided by the delegation model. As with any powerful tool, it is best to understand its strengths and weaknesses and use that understanding in determining when its use is justified and appropriate.

Creating Graphical User Interface Trait Editors

Although not covered in this paper, it is also possible to automatically create graphical wxPython or Tkinter based trait editors for applications using objects with traits. Refer to the “Chaco: A Python Plotting Package for Scientists and Engineers” paper for some examples of this feature of the traits package.

Conclusion

The traits package began as a subcomponent of the Chaco plotting package designed to provide the casual scientific or engineering user with programmatic access to a rich set of Python plotting objects without fear of accidentally "blowing their foot off". In the process of meeting those goals it has evolved into a complete Python package in its own right, providing the programmer with selective use of strong type checking while still preserving the flexibility and programmer friendliness that are two of Python's hallmarks.

Where to Get the Code

The traits package is available as part of the SciPy (Scientific Python) open source project at www.scipy.org and is covered by a BSD-style license. All of the code described in this paper is contained in the traits project in the www.scipy.org CVS repository.

 

The CVS repository can be accessed from a command shell using the commands:

 

cvsd:pserver:anonymous@scipy.org:/home/cvsroot login

cvsd:pserver:anonymous@scipy.org:/home/cvsroot co traits

 

It can also be browsed on-line using the ViewCVS facility. There is also a traits page at http://www.scipy.org/site_content/traits that contains recent traits source tarballs and a Windows installer program.