Credit: Sébastien Keim
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.
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
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
TextBlock
s 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.
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.