PyZine
 


Article Finder
People
Issue 4 - Revision 2  /   January 28, 2003 


 
  Py Links:
Latest Issue
Issue 08
Issue 07
Issue 06
Issue 05
Issue 04
Issue 02
Issue 01
Migration FAQ
 
 
Downloads
     
  Articles:
Throughout the quarter we cover topics of interest to Python developers.

  What's new in Python 2.3?

  Chaco Properties

  Software Holy Wars

  Creating Crossword Puzzles with Python



 
 
Downloads
     
  URLs
Things of Interest to Python Users

  opensourcexperts
  ZopeMag
  Daily Python
 
     

Illustration by Py Staff
Chaco Properties Cover
Chaco Properties

Chaco Properties
- Adding Properties to Python Classes
- - - - - - - - - - - -

By David C. Morrill | published November 17, 2003

print

Introduction

To begin, let's start with two simple questions and their answers:.

What is a property?

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

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

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

Why would we want to use properties?

Python is a weakly typed language, which as any experienced Python programmer knows has both good and bad points. The main purpose of the properties package is to help address cases where weak typing leads to problems. In particular, the motivation for properties 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 could write:

plotitem.color = 'red'
plotitem = 0xFF0000; or
plotitem = (1.0,0.0,0.0,1.0)

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 properties 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 properties module 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 properties described in this article work with any version of Python, including version 2.2 (and later), despite the fact that as of this version a new property language feature has been defined. The Python language property feature can be used to provide the same capabilities as the properties described here, but with more work on the part of the programmer. The properties 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 set the stage as regards the what and why of properties, let's roll up our sleeves and get into the good stuff: ¦defining and using properties.

Defining Properties

Too add properties to a Python class:

  1. Import the properties module.
  2. Derive the class from the properties module's HasProperties class or another class already derived from HasProperties.
  3. Define the class instance 'properties' by creating a class-level __properties__ dictionary describing the properties and their characteristics.

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

import properties
class Person ( properties.HasProperties ):
   __properties__ = { 'weight': 0.0 }

Note that the value associated with each __properties__ dictionary key determines the characteristics of the property identified by the key (see the next paragraph for details). Each key should be a string specifying the name of a property defined by the class and should follow the syntax rules for a valid Python attribute name. In a later article we will describe a special type of key value that can be used to specify an entire subclass of property names.

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

The properties module allows the creation of a wide variety of property types, ranging from very simple to very sophisticated. In all cases though, properties are defined in exactly the same manner: by associating a property definition with a property name in a class's __properties__ dictionary.

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

Defining Simple Properties

Perhaps the simplest type of property is the type introduced in the preceding section: a property defined by example. To define a property by example, simply specify adefault value for the property 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 property will have the specified value as its default, and in addition will only allow values of the same type to be assigned to the property.

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

For example:

class Person ( HasProperties ):
   __properties__ = {
'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 property definition occurs when the property can only have values which are instances of a particular class. In this case, you can provide as the property definition:

  • the class
  • an instance of the class

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

If you specify a class instance, then any value assigned to the property 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 property.

For example, continuing our previous example:

class Employee ( HasProperties ):
   __properties__ = {
'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' property
worker_bee.worker = Person( name = 'sam', age = 23 )
# Assigns valid value to 'worker' property
worker_bee.manager = Employee()
# Error, value is not an instance of class 'Person'

The third type of simple property 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 they do not all have to be of the same type.

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

For example:

class InventoryItem ( HasProperties ):
   __properties__ = {
'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 Properties

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

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

__properties__ = { 'property_name': Property( â€: ) }

The constructor for the Property class has several different forms:


Property( class )
Property( instance )
Property( default_value )
Property( default_value, other_value2, other_value3, â€:¦ )
Property([default_value, other_value2, other_value3, â€:¦ ])

Property( default_value, { python_type       |
			   constant_value    |
			   dictionary        |
			   class             |
			   function          |
			   property_handler  |
			   property_delegate |
			   property }+ )
			   Property( property_handler )
			   Property( property_delegate )

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

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

There are currently two keywords which are used by various property helper classes: desc: A string describing the intended meaning of the property. It is used in exceptions and fly-over help in user interface property sheets.

label: A string providing a human readable name for the property. It is used to label property values in a user interface property sheet.

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

The first five forms above:

Property( class )
Property( instance )
Property( default_value )
Property( default_value, other_value2, other_value3, … )
Property([default_value, other_value2, other_value3, … ])

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

The next form:

Property( default_value, { python_type       |
                           constant_value    |
                           dictionary        |
                           class             |
                           function          |
                           property          |
                           property_handler  |
                           property_delegate }+ )

is the most general case of a property definition. You specify a default value for the property followed by a list of one or more items describing legal values for the property (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 property value before it has been set).

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

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 property 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 property can have the constant as a legal value.

dictionary

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

class

Specifies that the property 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 property. More information about writing a property function is provided later.

property_handler

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

property_delegate

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

property

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

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

import types
class Nonsense ( HasProperties ):
   __properties__ = {
'rubbish': Property( 0.0, 
0.0, 'stuff', types.TupleType )
}

The Nonsense class has a rubbish property 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 property's legal values.

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

import types
class Nonsense ( HasProperties ):
   __properties__ = {
      'rubbish': Property( 0.0, 'stuff', types.TupleType )
   }

foo  = Nonsense()
oops = foo.rubbish # Error, generates following exception:
Traceback (most recent call last):
File "", line 1, in ?
File "properties.py", line 1073, in __getattr__
raise PropertyError, '%s %s' % ( str( excp )[:-1],
properties.PropertyError: 
The 'rubbish' property of a Nonsense instance must be of type 'tuple' or 'stuff', 
but a value of 0.0 was specified as the default value. 
The property 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 properties mechanism is the ability to automatically generate detailed exceptions when a program incorrectly uses or sets a property.

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

import types
class Nonsense ( HasProperties ):
   __properties__ = {
      'rubbish': Property( 0.0, 'stuff', types.TupleType,
                           desc = 'total rubbish' )
   }
foo  = Nonsense()
foo.rubbish = 1 # Error, generates following exception:
Traceback (most recent call last):
  File "", line 1, in ?
  File "properties.py", line 1090, in __setattr__
    raise PropertyError, excp
properties.PropertyError: 
The 'rubbish' property of a Nonsense instance specifies 'total rubbish' 
and must be of type 'tuple' or 'stuff', but a value of 1 was specified.
Mapped Properties

If a Property constructor contains one or more dictionaries, then the resulting property is called a mapped property. 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 property, and the other containing its corresponding value (i.e. the mapped value). The name of the mapped attribute is simply the base property name with an underscore appended to the end (see the example under "sally = Kid()" below).

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

true_boolean = Property( 'true', { 'true':  1,
                                   't':     1,
                                   'yes':   1,
                                   'y':     1,
                                   1:       1,
                                   'false': 0,
                                   'f':     0,
                                   'no':    0,
                                   'n':     0,
                                   0:       0 } )
false_boolean = Property( 'false', true_boolean )
class Kid ( HasProperties ):
__properties__ = {
'likes_ice_cream': true_boolean,
'likes_spiders':   false_boolean,
}

The Kid class has two properties: likes_ice_cream and likes_spiders. Because the true_boolean property uses a dictionary, both likes_ice_cream and likes_spiders are mapped properties, 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 property 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 properties from existing ones. In this case, the false_boolean property is created by re-using the true_boolean property and specifying a new default value. This is often an efficient way of reusing an existing property because it shares most of the internal state of the existing property without one's having to make a completely new copy of the property description.

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

  1. If not all values in the Property constructor are dictionaries, the non-dictionary values are copied directly to the mapped attribute (i.e. the mapping used is the identity mapping).
  2. If only dictionaries are used in the Property constructor and the composite mapping defined by the dictionaries is 1:1, the property is a reversible mapped property. 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 property's corresponding value.
  3. If the composite mapping is not 1:1, assigning to the mapped attribute has no effect on the base property. In this case it is not recommended to assign values to the mapped attribute, since it is possible for the base property and its mapped attribute to become inconsistent.

The first case is illustrated below:

class Balance ( HasProperties ):
__properties__ = { 
'sign': Property( 'positive', -1, 0, 1, 
                        { 'positive':  1,
                          'negative': -1 } )

In this example, the Balance class has a sign property 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' property 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 properties. As originally defined, these properties are not reverse-mapped properties 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 property definition, we can make the likes_ice_cream and likes_spiders properties reverse-mapped:

true_boolean = Property( 'yes', { 'yes': 1,
                                  'no':  0 } )

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

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
Property Functions

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

function ( object, name, value )

where:

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

The function is invoked whenever a value is assigned to the property. Normally the function does not need to know the object or property 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 property. 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 property.

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 property mechanism and mapped into a PropertyError 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 Property constructor to define a property 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 properties 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 property 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 property whenever an exception occurs assigning to the property. If the info attribute is not defined, the string 'a legal value' will be used in its place.

Putting this all together:

class DataBaseRecord ( HasProperties ):
   __properties__ = {
      'part_desc': Property( None, None, bounded_string )
   }
sprocket = DataBaseRecord()
sprocket.part_desc = 0 # Generates this exception:
Traceback (most recent call last):
  File "", line 1, in ?
  File "properties.py", line 1090, in __setattr__
    raise PropertyError, excp
properties.PropertyError: The 'part_desc' property of a DataBaseRecord 
instance must be a string no longer than 50 characters or 'None', 
but a numerical value of 0 was specified.

This illustrates how a function can be combined with other values in a Property constructor to create a composite property and how the function's info attribute is used when generating a PropertyError exception.

What's Next?

Although we've already covered a lot of ground in describing the capabilities provided by the Properties module, in fact we've only just scratched the surface of what can be done. Unfortunately, we've used about as much ink as we're allowed in one issue, and the rest will have to wait for another article.

A future article will explore some of the more advanced capabilities of the Properties module: The ability to define more sophisticated value checking using PropertyHandler classes. The ability to delegate the value of a property to another object in several different ways using PropertyDelegate objects.

The ability to automatically create sophisticated wxPython- or Tkinter-based graphical property sheets editors for an object directly from its property definitions.

Where to Get the Code

The Properties module is currently available as part of the Chaco Open Source scientific plotting project at www.scipy.org and is covered by a BSD-style license. All of the code described in this article is contained in a single file, properties.py, in the Chaco module of the www.scipy.org CVS repository.

The CVS repository can be accessed either via a command shell or using the ViewCVS facility. In either case, refer to the www.scipy.org web site for further information about accessing the source code.

There are several auxiliary modules used when creating wxPython- or Tkinter-based property sheet editors which are also available:

  • property_sheet.py
  • wxproperty_sheet.py
  • wxplot_property_sheet.py (Chaco-specific)
  • tkproperty_sheet.py
  • tkplot_propery_sheet.py (Chaco-specific)

The use of these modules was not covered here, but will be discussed in a follow-on article. If you would like to see some screenshots of the types of graphical property sheets that are easily created using the properties package, please visit the Chaco home page at http://www.scipy.org/site_content/chaco.

And if you would like to see some sophisticated uses of properties and property sheet editors in action, please download the entire Chaco package. All of the high-level Chaco plotting objects rely almost exclusively on the capabilities provided by the properties modules listed above.


David Morrill:

was born in Burlington, Vermont, USA and received a B.S. in Mathematics from Pratt Institute. He spent nearly a quarter of a century with IBM, working in development and research laboratories located in several different parts of the United States. Projects completed while at IBM ranged from mainframe telecommunications systems, to microprocessor-controlled bank check sorters, to program development environments and cross-platform, language neutral, object-oriented middleware. He is perhaps most noted (or notorious) for being the creator of TopView, the first multi-tasking operating system for the PC (1984), and DrDialog (1993), a visual programming environment still in use around the world today by die-hard OS/2 loyalists.

David currently works at Enthought, a small consulting company in Austin, Texas specializing in both web-based and scientific applications, such as Chaco, a Python-based Open Source scientific plotting package.


shim
shim

 Py is committed to bringing you great Python Articles.

shim
shim


Home   Subscribe   Migration FAQ   Contact PyZine   Write for PyZine   ZopeMag   opensourcexperts.com  

Reproduction of material from any of PyZine's pages without prior written permission is strictly prohibited. Copyright 2003 - 2005 PyZine Zope/Plone hosting by Nidelven IT