Recipe 20.11. Allowing Chaining of Mutating List Methods
Credit: Stephan Diehl, Alex Martelli
Problem
The methods of the
list type that mutate a list object in
placemethods such as append and
sortreturn None. To call
a series of such methods, you therefore need to use a series of
statements. You would like those methods to return
self to enable you to chain a
series of calls within a single expression.
Solution
A custom metaclass can offer an elegant approach to this task:
def makeChainable(func):
''' wrapp a method returning None into one returning self '''
def chainableWrapper(self, *args, **kwds):
func(self, *args, **kwds)
return self
# 2.4 only: chainableWrapper._ _name_ _ = func._ _name_ _
return chainableWrapper
class MetaChainable(type):
def _ _new_ _(mcl, cName, cBases, cDict):
# get the "real" base class, then wrap its mutators into the cDict
for base in cBases:
if not isinstance(base, MetaChainable):
for mutator in cDict['_ _mutators_ _']:
if mutator not in cDict:
cDict[mutator] = makeChainable(getattr(base, mutator))
break
# delegate the rest to built-in 'type'
return super(MetaChainable, mcl)._ _new_ _(mcl, cName, cBases, cDict)
class Chainable: _ _metaclass_ _ = MetaChainable
if _ _name_ _ == '_ _main_ _':
# example usage
class chainablelist(Chainable, list):
_ _mutators_ _ = 'sort reverse append extend insert'.split( )
print ''.join(chainablelist('hello').extend('ciao').sort( ).reverse( ))
# emits: oolliheca
Discussion
Mutator methods of mutable objects such as lists and dictionaries
work in place, mutating the object they're called
on, and return None. One reason for this behavior
is to avoid confusing programmers who might otherwise think such
methods build and return new objects. Returning
None also prevents you from
chaining a sequence of mutator calls, which some
Python gurus consider bad style because it can lead to very dense
code that may be hard to read.Some programmers, however, occasionally prefer the chained-calls,
dense-code style. This style is particularly useful in such contexts
as lambda forms and list comprehensions. In these contexts, the
ability to perform actions within an expression, rather than in
statements, can be crucial. This recipe shows one way you can tweak
mutators' return values to allow chaining. Using a
custom metaclass means the runtime overhead of introspection is paid
only rarely, at class-creation time, rather than repeatedly. If
runtime overhead is not a problem for your application, it may be
simpler for you to use a delegating wrapper idiom that was posted to
comp.lang.python by Jacek Generowicz:
class chainable(object):The use of this wrapper is quite similar to that of classes obtained
def _ _init_ _(self, obj):
self.obj = obj
def _ _iter_ _(self):
return iter(self.obj)
def _ _getattr_ _(self, name):
def proxy(*args, **kwds):
result = getattr(self.obj, name)(*args, **kwds)
if result is None: return self
else: return result
# 2.4 only: proxy._ _name_ _ = name
return proxy
by the custom metaclass presented in this recipe's
Solutionfor example:
print ''.join(chainable(list('hello')).extend('ciao').sort( ).reverse( ))
# emits: oolliheca
See Also
Library Reference and Python in a
Nutshell docs on built-in type list
and special methods _ _new_ _ and _
_getattr_ _.