5.17 Managing Options
Credit: Sébastien Keim
5.17.1 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.
5.17.2 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
5.17.3 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.
5.17.4 See Also
The section on emulating numeric types in the Reference
Manual.
|