Recipe 6.17. Implementing the Null Object Design Pattern
Credit: Dinu C. Gherman, Holger Krekel
Problem
You
want to reduce the need for conditional statements in your code,
particularly the need to keep checking for special cases.
Solution
The usual placeholder object for
"there's nothing
here" is None, but we may be able
to do better than that by defining a class meant exactly to act as
such a placeholder:
class Null(object):
"" Null objects always and reliably "do nothing." ""
# optional optimization: ensure only one instance per subclass
# (essentially just to save memory, no functional difference)
def _ _new_ _(cls, *args, **kwargs):
if '_inst' not in vars(cls):
cls._inst = type._ _new_ _(cls, *args, **kwargs)
return cls._inst
def _ _init_ _(self, *args, **kwargs): pass
def _ _call_ _(self, *args, **kwargs): return self
def _ _repr_ _(self): return "Null( )"
def _ _nonzero_ _(self): return False
def _ _getattr_ _(self, name): return self
def _ _setattr_ _(self, name, value): return self
def _ _delattr_ _(self, name): return self
Discussion
You can use an instance of the Null class instead of
the primitive value None. By using such an
instance as a placeholder, instead of None, you
can avoid many conditional statements in your code and can often
express algorithms with little or no checking for special values.
This recipe is a sample implementation of the Null Object Design
Pattern. (See B. Woolf, "The Null Object
Pattern" in Pattern Languages of
Programming [PLoP 96, September 1996].)This recipe's Null class ignores
all parameters passed when constructing or calling instances, as well
as any attempt to set or delete attributes. Any call or attempt to
access an attribute (or a method, since Python does not distinguish
between the two, calling _ _getattr_ _ either way)
returns the same Null instance (i.e.,
selfno reason to create a new instance).
For example, if you have a computation such as:
def compute(x, y):and you use it like this:
try:
lots of computation here to return some appropriate object
except SomeError:
return None
for x in xs:you can usefully change the computation to:
for y in ys:
obj = compute(x, y)
if obj is not None:
obj.somemethod(y, x)
def compute(x, y):and thus simplify its use down to:
try:
lots of computation here to return some appropriate object
except SomeError:
return Null( )
for x in xs:The point is that you don't need to check whether
for y in ys:
compute(x, y).somemethod(y, x)
compute has returned a real result or an instance
of Null: even in the latter case, you can safely
and innocuously call on it whatever method you want. Here is another,
more specific use case:
log = err = Null( )This obviously avoids the usual kind of
if verbose:
log = open('/tmp/log', 'w')
err = open('/tmp/err', 'w')
log.write('blabla')
err.write('blabla error')
"pollution" of your code from
guards such as if verbose: strewn all over the
place. You can now call log.write('bla'), instead
of having to express each such call as if log is not None:
log.write('bla').In the new object model, Python does not call _ _getattr_
_ on an instance for any special methods needed to perform
an operation on the instance (rather, it looks up such methods in the
instance class' slots). You may have to take care
and customize Null to your
application's needs regarding operations on null
objects, and therefore special methods of the null
objects' class, either directly in the
class' sources or by subclassing it appropriately.
For example, with this recipe's
Null, you cannot index Null
instances, nor take their length, nor iterate on them. If this is a
problem for your purposes, you can add all the special methods you
need (in Null itself or in an appropriate subclass)
and implement them appropriatelyfor example:
class SeqNull(Null):Similar considerations apply to several other operations.The key goal of Null objects is to provide an
def _ _len_ _(self): return 0
def _ _iter_ _(self): return iter(( ))
def _ _getitem_ _(self, i): return self
def _ _delitem_ _(self, i): return self
def _ _setitem_ _(self, i, v): return self
intelligent replacement for the often-used primitive value
None in Python. (Other languages represent the
lack of a value using either null or a null pointer.) These
nobody-lives-here markers/placeholders are used for many purposes,
including the important case in which one member of a group of
otherwise similar elements is special. This usage usually results in
conditional statements all over the place to distinguish between
ordinary elements and the primitive null (e.g.,
None) value, but Null objects
help you avoid that.Among the advantages of using Null objects are the
following:
- Superfluous conditional statements can be avoided by providing a
first-class object alternative for the primitive value
None, thereby improving code readability. - Null objects can act as placeholders for objects
whose behavior is not yet implemented. - Null objects can be used polymorphically with
instances of just about any other class (perhaps needing suitable
subclassing for special methods, as previously mentioned). - Null objects are very predictable.
The one serious disadvantage of Null is that it can
hide bugs. If a function returns None, and the
caller did not expect that return value, the caller most likely will
soon thereafter try to call a method or perform an operation that
None doesn't support, leading to
a reasonably prompt exception and traceback. If the return value that
the caller didn't expect is a Null,
the problem might stay hidden for a longer time, and the exception
and traceback, when they eventually happen, may therefore be harder
to reconnect to the location of the defect in the code. Is this
problem serious enough to make using Null
inadvisable? The answer is a matter of opinion. If your code has
halfway decent unit tests, this problem will not arise; while, if
your code lacks decent unit tests, then using
Null is the least of your
problems. But, as I said, it boils down to a matter of opinions. I
use Null very widely, and I'm
extremely happy with the effect it has had on my productivity.The Null class as presented in this recipe uses a
simple variant of the "Singleton"
pattern (shown earlier in Recipe 6.15), strictly for optimization
purposesnamely, to avoid the creation of numerous passive
objects that do nothing but take up memory. Given all the previous
remarks about customization by subclassing, it is, of course, crucial
that the specific implementation of
"Singleton" ensures a
separate instance exists for each subclass of
Null that gets instantiated. The number of
subclasses will no doubt never be so high as to eat up substantial
amounts of memory, and anyway this per-subclass distinction can be
semantically crucial.
See Also
B. Woolf, "The Null Object Pattern"
in Pattern Languages of Programming (PLoP 96,
September 1996), http://www.cs.wustl.edu/~schmidt/PLoP-96/woolf1.ps.gz;
Recipe 6.15.