| Traits User Manual | ||
| Previous: 1 Introduction | Table of Contents | Next: 3 Advanced Topics |
2 Defining Traits: Initialization and ValidationUsing the Traits package in a Python program requires the following operations: 1. Import the names you need from the Traits package (enthought.traits.api). 2. Define the traits you want to use. 3. Define classes derived from HasTraits (or a subclass of HasTraits), with attributes that use the traits you have defined. In practice, steps 2 and 3 are often combined by defining traits in-line in an attribute definition. This strategy is used in many examples in this guide. However, you can also define traits independently, and reuse the trait definitions across multiple classes and attributes. Type-checked method signatures typically use independently defined traits. In order to use trait attributes in a class, the class must inherit from the HasTraits class in the Traits package (or from a subclass of HasTraits). The following example defines a class called Person that has a single trait attribute weight, which is initialized to 150.0 and can only take floating point values. # minimal.py --- Minimal example of using traits. from enthought.traits.api import HasTraits, Float
class Person(HasTraits): weight = Float(150.0) In this example, the attribute named weight specifies that the class has a corresponding trait called weight. The value associated with the attribute weight (i.e., Float(150.0)) specifies a predefined trait provided with the Traits package, which requires that values assigned be of the standard Python type float. The value 150.0 specifies the default value of the trait. The value associated with each class-level attribute determines the characteristics of the instance trait identified by the attribute name. For example: >>> from minimal import Person >>> joe = Person() >>> print joe.weight 150.0 >>> joe.weight = 161.9 # OK to assign a float >>> joe.weight = 162 # OK to assign an int >>> joe.weight = 'average' # Error to assign a string Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\wrk\src\lib\enthought\traits\trait_handlers.py", line 89, in error raise TraitError, ( object, name, self.info(), value ) enthought.traits.trait_errors.TraitError: The 'weight' trait of a Person instance must be a value of type 'float', but a value of average was specified. In this example, joe is an instance of the Person class defined in the previous example. The joe object has an instance attribute weight, whose initial value is the default value of the Person.weight trait (150.0), and whose assignment is governed by the Person.weight trait’s validation rules. Assigning an integer to weight is acceptable because there is no loss of precision (but assigning a float to an Int trait would cause an error). The Traits package allows creation of a wide variety of trait types, ranging from very simple to very sophisticated. The following section presents some of the simpler, more commonly used forms. 2.1 Simple Trait DefinitionsOf the many methods for defining traits, some of the simplest methods include: · Using predefined traits · Defining by example · Defining a list of all possible values Using a predefined trait was illustrated at the beginning of this section by using the Float() function to create an attribute whose value must be a floating point value. 2.1.1 Predefined TraitsThe Traits package includes a number of predefined traits for commonly used Python data types. Most of these predefined traits have factory functions that can take an argument, which becomes the default value for the trait being defined. For example: account_balance = Float(10.0) The predefined traits also have an appropriate built-in default value for the corresponding type. If you want to use the built-in default value for the type, you can use the predefined trait name without parentheses after it. The statements in the following example are equivalent: account_balance = Float(0.0) account_balance = Float The statements are equivalent because 0.0 is the built-in default value for the predefined Float trait. 2.1.1.1 Predefined Traits for Simple TypesThere are two categories of predefined traits corresponding to simple types: those that coerce values, and those that cast values. These categories vary in the way that they handle assigned values that do not match the type explicitly defined for the trait. However, they are similar in terms of the Python types they correspond to, and their built-in default values, as listed in Table 1. Table 1 Predefined defaults for simple types
2.1.1.1.1 Trait Type CoercionFor trait attributes defined using the predefined “coercing” traits, if a value is assigned to a trait attribute that is not of the type defined for the trait, but it can be coerced to the required type, then the coerced value is assigned to the attribute. If the value cannot be coerced to the required type, a TraitError exception is raised. Only widening coercions are allowed, to avoid any possible loss of precision. Table 2 lists traits that coerce values, and the types that each coerces. Table 2 Type coercions permitted for coercing traits
2.1.1.1.2 Trait Type CastingFor trait attributes defined using the predefined “casting” traits, if a value is assigned to a trait attribute that is not of the type defined for the trait, but it can be cast to the required type, then the cast value is assigned to the attribute. If the value cannot be cast to the required type, a TraitError exception is raised. Internally, casting is done using the Python built-in functions for type conversion: · bool() · complex() · float() · int() · str() · unicode() The following example illustrates the difference between coercing traits and casting traits. >>> from enthought.traits.api import HasTraits, Float, CFloat >>> class Person ( HasTraits ): ... weight = Float ... cweight = CFloat >>> >>> bill = Person() >>> bill.weight = 180 # OK, coerced to 180.0 >>> bill.cweight = 180 # OK, cast to float(180) >>> bill.weight = '180' # Error, invalid coercion Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\wrk\src\lib\enthought\traits\trait_handlers.py", line 89, in error raise TraitError, ( object, name, self.info(), value ) enthought.traits.trait_errors.TraitError: The 'weight' trait of a Person instance must be a value of type 'float', but a value of 180 was specified. >>> >>> bill.cweight = '180' # OK, cast to float('180') >>> print bill.cweight 180.0 2.1.1.2 Other Predefined TraitsThe Traits package provides a number of other predefined traits besides those for simple types, corresponding to other commonly used data types, which are listed in Table 3. Refer to the Traits API Reference, in the section for the module enthought.traits.traits, for details. Most can be used either as factory functions accepting a default value as an argument, or as simple names, which use their built-in default values. Table 3 Predefined traits and trait factories beyond simple types
2.1.1.3 ThisOne predefined trait that merits special explanation is This. Use This for attributes whose values must be of the same class (or a subclass) as the enclosing class. The default value is None. The following is an example of using This: # this.py --- Example of This predefined trait from enthought.traits.api import HasTraits, This
class Employee(HasTraits): manager = This This example defines an Employee class, which has a manager trait attribute, which accepts only other Employee instances as its value. It might be more intuitive to write the following: # bad_self_ref.py --- Non-working example with self- referencing # class definition from enthought.traits.api import HasTraits, Trait
class Employee(HasTraits): manager = Trait(Employee) However, the Employee class is not fully defined at the time that the manager attribute is defined. Handling this common design pattern is the main reason for providing the This trait. Note that if a trait attribute is defined using This on one class and is referenced on an instance of a subclass, the This trait verifies values based on the class of the instance being referenced. For example: >>> from enthought.traits.api import HasTraits, This >>> class Employee(HasTraits): ... manager = This ... >>> class Executive(Employee): ... pass ... >>> fred = Employee() >>> mary = Executive() >>> fred.manager = mary >>> # This is OK, because fred's manager can be an instance >>> # of Employee or any subclass. >>> mary.manager = fred 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 'manager' trait of an Executive instance must be an instance of the same type as the receiver, but a value of <__main__.Employee object at 0x00994330> was specified. 2.1.2 Defining By ExampleIn addition to predefined traits, another simple form of trait definition is defining a trait by example. To define a trait by example, call the Trait() function with the default value as the only argument. The Trait() function infers the type of the trait from the type of the specified default value. The following two statements are equivalent: weight = Float(150.0) weight = Trait(150.0) A trait defined by example has the specified value as its default value, and allows only values of the same type as the default value to be assigned. The default value must be one of the simple Python data types (plain integer, long integer, floating point number, complex number, string, Unicode string, or Boolean). 2.1.3 List of Possible ValuesIn addition to a default value, a trait definition can specify an exhaustive set of all permitted values, as arguments to the Trait() function. The values must all be of simple Python data types, such as strings, integers, and floats, but they do not have to all be of the same type. This list of values can be a typical parameter list, an explicit (bracketed) list, or a variable whose type is list. A trait defined in this fashion can accept only values that are contained in the list of permitted values. The default value is the first value specified; it is not considered a valid value for assignment unless it is repeated after the first position in the argument list. >>> from enthought.traits.api import HasTraits, Str, Trait >>> class InventoryItem(HasTraits): ... name = Str # String value, default is '' ... stock = Trait(None, 0, 1, 2, 3, 'many') ... # Enumerated list, default value is ... #'None' ... >>> hats = InventoryItem() >>> hats.name = 'Stetson'
>>> print '%s: %s' % (hats.name, hats.stock) Stetson: None
>>> hats.stock = 2 # OK >>> hats.stock = 'many' # OK >>> hats.stock = 4 # Error, value is not in \ >>> # permitted list 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 'stock' trait of an InventoryItem instance must be None or 0 or 1 or 2 or 3 or 'many', but a value of 4 was specified. This example defines an InventoryItem class, with two trait attributes, name, and stock. The name attribute is simply a string. The stock attribute has an initial value of None, and can be assigned the values 0, 1, 2, 3, and 'many'. The example then creates an instance of the InventoryItem class named hats, and assigns values to its attributes. 2.2 The Trait() FunctionThe predefined traits such as those listed in Table 3 are handy shortcuts for commonly used types. However, the Traits package provides a generic facility for defining complex or customized traits: the Trait() function. The Trait() function
has many forms, many of which are redundant with the predefined shortcut
traits. For example, the simplest form The Trait() function’s variety of forms allow a wide variety of flexible and powerful traits to be defined. Table 4 shows the complete set of forms understood by the Trait() function: Table 4 Forms of the Trait() function
The following is an example of a compound trait with multiple criteria. multiple_criteria.py -- Example of multiple criteria in a trait # definition from enthought.traits.api import HasTraits, Trait from types import TupleType
class Nonsense(HasTraits): rubbish = Trait(0.0, 0.0, 'stuff', TupleType) The Nonsense class has a rubbish trait, which has a default value of 0.0, and can have any of the following values: · The constant float value 0.0 · The constant string value 'stuff' · Any Python tuple Note that in this case, it is necessary to specify 0.0 twice: the first occurrence defines the default value, and the second occurrence specifies 0.0 as one of the values that can be assigned to the trait. 2.2.1 Compound Trait ParametersThe items listed as categories of values for a compound trait (in Table 4, Form 9) merit some further explanation. · type―See Section 2.2.1.1, “Type”. · constant_value―See Section 2.2.1.2, “Constant Value”. · dictionary―See Section 2.3, “Mapped Traits”.. · class―Specifies that the trait value must be an instance of the specified class or one of its subclasses. · function―See Section 2.4, “Validator Functions”.. · trait_handler―See Section 2.5, “Trait Handlers”.. · trait―Another trait object can be passed as a parameter; any value that is valid for the specified trait is also valid for the trait referencing it. 2.2.1.1 TypeA type parameter to the Trait() function can be any of the following standard Python types: · str or StringType · unicode or UnicodeType · int or IntType · long or LongType · float or FloatType · complex or ComplexType · bool or BooleanType · list or ListType · tuple or TupleType · dict or DictType · FunctionType · MethodType · ClassType · InstanceType · TypeType · NoneType Specifying one of these types means that the value must be of the corresponding Python type. 2.2.1.2 Constant ValueA constant_value parameter to the Trait() function can be any constant belonging to one of the following standard Python types: · NoneType · int · long · float · complex · bool · str · unicode Specifying a constant means that the trait can have the constant as a valid value. Note that Form 2 in Table 4 is a special case of the general form, in which the arguments consist of a default value followed by a series of constant values. 2.2.2 KeywordsAll forms of the Trait() function accept both predefined and arbitrary keyword arguments. The value of each keyword argument becomes bound to the resulting trait object as the value of an attribute having the same name as the keyword. This feature lets you associate metadata with a trait. The following predefined keywords are accepted by the Trait() function: · desc: A string describing the intended meaning of the trait. It is used in exception messages and fly-over help in user interface trait editors. · label: A string providing a human-readable name for the trait. It is used to label trait attribute values in user interface trait editors. · editor: Specifies an instance of a subclass of TraitEditor to use when creating a user interface editor for the trait. Refer to the Traits UI User Guide for more information on trait editors. · rich_compare: A Boolean indicating whether the basis for considering a trait attribute value to have changed is a “rich” comparision (True, the default), or simple object identity (False). This attribute can be useful in cases where a detailed comparison of two objects is very expensive, or where you do not care if the details of an object change, as long as the same object is used. For example: # keywords.py --- Example of trait keywords from enthought.traits.api import HasTraits, Trait
class Person(HasTraits): first_name = Trait('', desc='first or personal name', label='First Name') last_name = Trait('', desc='last or family name', label='Last Name') In this example, in a user interface editor for a Person object, the labels “First Name” and “Last Name” would be used for entry fields corresponding to the first_name and last_name trait attributes. If the user interface editor supports rollover tips, then the first_name field would display “first or personal name” when the user moves the mouse over it; the last_name field would display “last or family name” when moused over. 2.3 Mapped TraitsIf the Trait() function is called with parameters that include 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 whose value is a key in the dictionary used to define the trait, and the other containing its corresponding value (i.e., the mapped or shadow value). The name of the shadow attribute is simply the base attribute name with an underscore appended. Mapped traits can be used to allow a variety of user-friendly input values to be mapped to a set of internal, program-friendly values. The following examples illustrates mapped traits that map color names to tuples representing red, green, blue, and transparency values: # mapped.py --- Example of a mapped trait from enthought.traits.api import HasTraits, Trait
standard_color = Trait ('black', {'black': (0.0, 0.0, 0.0, 1.0), 'blue': (0.0, 0.0, 1.0, 1.0), 'cyan': (0.0, 1.0, 1.0, 1.0), 'green': (0.0, 1.0, 0.0, 1.0), 'magenta': (1.0, 0.0, 1.0, 1.0), 'orange': (0.8, 0.196, 0.196, 1.0), 'purple': (0.69, 0.0, 1.0, 1.0), 'red': (1.0, 0.0, 0.0, 1.0), 'violet': (0.31, 0.184, 0.31, 1.0), 'yellow': (1.0, 1.0, 0.0, 1.0), 'white': (1.0, 1.0, 1.0, 1.0), 'transparent': (1.0, 1.0, 1.0, 0.0) } )
red_color = Trait ('red', standard_color)
class GraphicShape (HasTraits): line_color = standard_color fill_color = red_color The GraphicShape class has two attributes: line_color and fill_color. These attributes are defined in terms of the standard_color trait, which uses a dictionary. The standard_color trait is a mapped trait, which means that each GraphicShape instance has two shadow attributes: line_color_ and fill_color_. Any time a new value is assigned to either line_color or fill_color, the corresponding shadow attribute is updated with the value in the dictionary corresponding to the value assigned. For example: >>> import mapped >>> my_shape1 = mapped.GraphicShape() >>> print my_shape1.line_color, my_shape1.fill_color black red >>> print my_shape1.line_color_, my_shape1.fill_color_ (0.0, 0.0, 0.0, 1.0) (1.0, 0.0, 0.0, 1.0) >>> my_shape2 = mapped.GraphicShape() >>> my_shape2.line_color = 'blue' >>> my_shape2.fill_color = 'green' >>> print my_shape2.line_color, my_shape2.fill_color blue green >>> print my_shape2.line_color_, my_shape2.fill_color_ (0.0, 0.0, 1.0, 1.0) (0.0, 1.0, 0.0, 1.0) This example shows how a mapped trait can be used to create a user-friendly attribute (such as line_color) and a corresponding program-friendly shadow attribute (such as line_color_). The shadow attribute is program-friendly because it is usually in a form that can be directly used by program logic. There are a few other points to keep in mind when creating a mapped trait: · If not all values passed to the Trait() function are dictionaries, the non-dictionary values are copied directly to the shadow attribute (i.e., the mapping used is the identity mapping). · Assigning directly to a shadow attribute (the attribute with the trailing underscore in the name) is not allowed, and raises a TraitError. 2.4 Validator FunctionsRather than directly specifying legal values for a trait, you can instead specify a reference to a validator function as an argument to the Trait() function. A function reference is one of the items that permitted as an argument to Trait() in its most general form. The validator function determines at run time whether a value being assigned to the attribute is a legal value. Such a function must have the following prototype: function( object, name, value ) In this prototype: · function is the name of the function. · object is the object whose attribute is being assigned to. · name is the name of the attribute being assigned to. · value is the value being assigned to the attribute. The function is invoked whenever a value is assigned to the attribute. Normally the function does not need to know the object or attribute name being assigned to, but these parameters are provided in case the testing performed by the function is context-dependent. The function indicates a value is valid by returning normally. The function can return the original value passed to it, or it can return any other value, usually derived from the original value. The returned value is assigned to the attribute. 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. For example: # validator.py --- Example of a validator 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:]) The bounded_string() function can be passed to the Trait() function 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 validator functions to be as descriptive as possible, you can attach a short string describing the values accepted by the function as the info attribute of the function. For example, continuing the bounded_string example: bounded_string.info = The string contained in the function’s info attribute is merged with other information about the trait whenever an exception occurs while assigning a value to the trait attribute. If the info attribute is not defined, the string 'a legal value' is used in its place. The following example shows how a validator function can be combined with other values passed to the Trait() function to create a compound trait, and how the validator function's info attribute is used when generating a TraitError exception. >>> from enthought.traits.api import HasTraits, Trait >>> from validator import bounded_string >>> >>> bounded_string.info = 'a string no longer than 50 characters' >>> class DatabaseRecord(HasTraits): ... part_desc = Trait(None, None, bounded_string) ... >>> sprocket = DatabaseRecord() >>> sprocket.part_desc = 0
Traceback (most recent call last): File "<pyshell#29>", line 1, in -toplevel- sprocket.part_desc = 0 File "c:\wrk\src\lib\enthought\traits\trait_handlers.py", line 90, in error raise TraitError, ( object, name, self.info(), value ) 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. 2.5 Trait HandlersAs an alternative to defining a trait validator function, you can use a predefined trait handler class or write your own. A trait handler is an instance of the TraitHandler class, or of a subclass, whose task is to verify the correctness of values assigned to object traits. When a value is assigned to an object trait that has a trait handler, the trait handler’s validate() method checks the value, and assigns that value or a computed value, or raises a TraitError if the assigned value is not valid. Trait handlers have several advantages over trait validator functions, due to being classes: · They can have constructors and state. Therefore, you can use them to create parameterized types. · They can have multiple methods, whereas validator functions have only one callable interface. This feature allows more flexibility in their implementation, and allows them to handle a wider range of cases, such as interactions with other components. The Traits package provides a number of predefined TraitHandler subclasses, which handle a wide variety of trait definition situations. (In fact, all of the trait definitions mentioned previously rely ultimately on one or more of the predefined TraitHandler subclasses.) A few of the predefined trait handler classes are described in the following sections. For a complete list and descriptions of predefined TraitHandler subclasses, refer to the Traits API Reference, in the section on the enthought.traits.trait_handlers module. You can also define your own trait handler class. For more information, refer to Section 3.1, “Custom Trait Handlers“. 2.5.1 TraitStringAn instance of the TraitString class ensures that a trait attribute value is a string that satisfies some additional, optional constraints. The constructor for TraitString has the following form: TraitString(self, minlen=0, maxlen=sys.maxint, regex='') In this constructor, minlen and maxlen are the minimum and maximum lengths allowed for the trait attribute's string value. The regex parameter is a string defining a Python regular expression that the string value must match. The TraitString handler first coerces the value being assigned to a string, provided that the value is a Python int, long, float, bool, or complex value. Assigning values of other non-string types results in a TraitError. The handler then makes sure that the resulting string is within the specified length range and that it matches the specified regular expression. For example: # traitstring.py --- Example of TraitString trait handler class from enthought.traits.api import HasTraits, Trait, TraitString
class Person(HasTraits): name=Trait('', TraitString(maxlen=50, regex=r'^[A-Za-z]*$')) This example defines a Person class with a name trait attribute, which must be a string of between 0 and 50 characters that consist only of upper and lower case letters. 2.5.2 TraitPrefixListThe TraitPrefixList handler accepts not only a specified set of strings as values, but also any unique prefix substring of those values. The value assigned to the trait attribute is the full string that the substring matches. For example: >>> from enthought.traits.api import HasTraits, Trait >>> from enthought.traits.api import TraitPrefixList >>> class Alien(HasTraits): ... heads = Trait('one', TraitPrefixList(['one','two','three'])) ... >>> alf = Alien() >>> alf.heads = 'o' >>> print alf.heads one >>> alf.heads = 'tw' >>> print alf.heads two >>> alf.heads = 't' # Error, not a unique prefix Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\wrk\src\lib\enthought\traits\trait_handlers.py", line 601, in validate self.error( object, name, self.repr( value ) ) 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 'heads' trait of an Alien instance must be 'one' or 'two' or 'three' (or any unique prefix), but a value of 't' was specified. 2.5.3 TraitPrefixMapThe TraitPrefixMap handler combines the TraitPrefixList with mapped traits. Its constructor takes a parameter that is a dictionary whose keys are strings. A string is a valid value if it is a unique prefix for a key in the dictionary. The value assigned is the dictionary value corresponding to the matched key. The following example uses TraitPrefixMap to define a Boolean trait that accepts any prefix of 'true', 'yes', 'false', or 'no', and maps them to 1 or 0. # traitprefixmap.py --- Example of TraitPrefixMap handler from enthought.traits.api import Trait, TraitPrefixMap
boolean_map = Trait('true', TraitPrefixMap( { 'true': 1, 'yes': 1, 'false': 0, 'no': 0 } ) ) |
| Previous: 1 Introduction | Table of Contents | Next: 3 Advanced Topics |
| Traits User Manual | ||