Managing Options

Credit: Sébastien Keim

Problem

You have classes that need vast numbers of options to be passed to their constructors for configuration purposes. This often happens with GUI toolkits in particular.

Solution

We can model the options with a suitable class:

class Options:
    def _ _init_ _(self, **kw):
        self._ _dict_ _.update(kw)

    def _ _lshift_ _(self, other):
        """ overloading operator << """
        s = self._ _copy_ _(  )
        s._ _dict_ _.update(other._ _dict_ _)
        return s

    def _ _copy_ _(self):
        return self._ _class_ _(**self._ _dict_ _)

and then have all classes using options inherit from the following class:

class OptionsUser:
    """ Base class for classes that need to use options """

    class OptionError(AttributeError): pass

    def initOptions(self, option, kw):
        """ To be called from the derived class constructor.
        Puts the options into object scope. """
        for k, v in option._ _dict_ _.items() + kw.items(  ):
            if not hasattr(self._ _class_ _, k):
                raise self.OptionError, "invalid option " + k
            setattr(self, k, v)

    def reconfigure(self, option=Options(  ), **kw):
        """ used to change options during object life """
        self.initOptions(option, kw)
        self.onReconfigure(self)

    def onReconfigure(self):
        """ To be overloaded by derived classes. Called by the reconfigure
        method or from outside after direct changes to option attributes. """
        pass

Discussion

To explain why you need this recipe, let’s start with an example:

class TextBlock:
    def _ _init_ _ (self, font='Times', size=14, color=(0,0,0), height=0,
        width=0, align='LEFT', lmargin=1, rmargin=1)
    ...

If you have to instantiate several objects with the same parameter values, your first action might be to repeat these values each time:

block1 = TextBlock(font='Arial', size=10, color=(1,0,0), height=20, width=200)
block2 = TextBlock(font='Arial', size=10, color=(1,0,0), height=80, width=100)
block3 = TextBlock(font='Courier', size=12, height=80, width=100)

This isn’t a particularly good solution, though, as you are duplicating code with all the usual problems. For example, when any change is necessary, you must hunt down all the places where it’s needed, and it’s easy to go wrong. The frequent mistake of duplicating code is also known as the antipattern named "copy-and-paste coding.”

A much better solution is to reuse code by inheritance rather than by copy and paste. With this Options recipe, you can easily avoid copy-and-paste:

stdBlockOptions = Options(font='Arial', size=10, color=(1,0,0), 
    height=80, width=100)
block1 = TextBlock(stdBlockOptions, height=20, width=200)
block2 = TextBlock(stdBlockOptions)
block3 = TextBlock(stdBlockOptions, font='Courier', size=12)

This feels a lot like using a stylesheet in a text processor. You can change one characteristic for all of your objects without having to copy this change in all of your declarations. The recipe also lets you specialize options. For example, if you have many TextBlocks to instantiate in Courier size 12, you can create:

courierBlockOptions = stdBlockOptions << Options(font='Courier', size=12)

Then any changes you make to the definition of stdBlockOptions change courierBlockOptions, except for size and font, which are specialized in the courierBlockOptions instance.

To create a class that accepts Options objects, your class should inherit from the OptionsUser class. You should define default values of options as static members, that is, attributes of the class object itself. And finally, the constructor of your class should call the initOptions method. For example:

class MyClass(OptionsUser):
    # options specification (default values)
    length = 10
    width = 20
    color = (0,0,0)
    xmargin = 1
    ymargin = 1

    def _ _init_ _ (self, opt=Options(  ), **kw):
        """ instance-constructor """
        self.initOptions(opt, kw)

The constructor idiom is intended to provide backward compatibility and ease of use for your class, as the specification of an Options object is optional, and the user can specify options in the constructor even if an Options object is specified. In other words, explicitly specified options override the content of the Options object.

You can, of course, adapt this recipe if your constructor needs parameters that can’t be sent as options. For example, for a class related to the Tkinter GUI, you would probably have a constructor signature such as:

def _ _init_ _(self, parentFrame, opt=Options(  ), **kw):

If you have many classes with the same default options, you should still use derivation (inheritance) for optimal reuse:

class MyDefaultOptions(OptionsUser):
    # options specification (default values)
    length=10
    width=20
    color=(0,0,0)

class MyClass(MyDefaultOptions):
    # options specification (specific options or additional default values)
    color=(1,0,0)
    xmargin = 1
    ymargin = 1

    def _ _init_ _(self, opt=Options(  ), **kw):
        """ instance-constructor """
        self.initOptions(opt,kw)

To change an instance object’s options at runtime, you can use either direct access to the options (object.option = value) or the reconfigure method (object.reconfigure(option=value)). The reconfigure method is defined in the OptionsUser class and accepts both an Options object and/or named parameters. To detect the change of an option at runtime, the reconfigure method calls the onReconfigure method. You should override it in your classes to do whatever is appropriate for your application’s specific needs. Direct access, however, cannot be handled automatically in a totally general and safe way, so you should ask your user to call the onReconfigure method to signal option changes.

There are several design choices in this recipe that deserve specific discussion. I used the << operator for overloading options because I wanted to avoid the problems caused by collision between a method name and an option in the Options class. So normal identifiers were not appropriate as method names. This left two possible solutions: using an operator or using an external function. I decided to use an operator. My first idea was to use the + operator, but when I started to deal with it, I discovered that it was a mistake, because overloading options isn’t a commutative operation. So I decided to use the << operator; because it is mostly unused in Python, its standard meaning isn’t commutative, and I found that its picture fit quite well with the overloading-option notion.

I put options in the class scope because this practice has the great benefit of improving default-options specification. I haven’t found a nonugly implementation for this, except for putting options in class scope, which allows direct access to options.

I used setattr in the initOptions method, even though a direct copy of _ _dict_ _ would substantially improve performance. But a class can emulate an option with the class’s _ _getattr_ _ and _ _setattr_ _ methods. And in Python 2.2, we now have getter and setter methods for specific data attributes. These all work with the setattr approach, but they would not work right if I used _ _dict_ _.update( ) instead. So my approach is more general and also fully compatible with Python 2.2’s new-style classes.

Finally, I chose to raise an exception when an option has no default value. When I first started to test the Options module, I once used size instead of length as the name of an option. Of course, everything worked well, except that my length option wasn’t initialized. It took me quite a long time to find the mistake, and I wonder what could happen if the same mistake happened in a large hierarchy of options with much overloading. So I do think that it is important to check for this.

See Also

The section on emulating numeric types in the Reference Manual.

Get Python Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.