Recipe6.17.Implementing the Null Object Design Pattern


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):     try:          lots of computation here to return some appropriate object     except SomeError:         return None

and you use it like this:

for x in xs:     for y in ys:         obj = compute(x, y)         if obj is not None:             obj.somemethod(y, x)

you can usefully change the computation to:

def compute(x, y):     try:         lots of computation here to return some appropriate object     except SomeError:         return Null( )

and thus simplify its use down to:

for x in xs:     for y in ys:         compute(x, y).somemethod(y, x)

The point is that you don't need to check whether 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( ) if verbose:    log = open('/tmp/log', 'w')    err = open('/tmp/err', 'w') log.write('blabla') err.write('blabla error')

This obviously avoids the usual kind of "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):     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

Similar considerations apply to several other operations.

The key goal of Null objects is to provide an 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.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net