A frequently missed feature of built-ins like lists and dicts is the ability to chain method calls like this:
x = []
x.append(1).append(2).append(3).reverse().append(4)
# x now equals [3, 2, 1, 4]
Unfortunately this doesn't work, as mutator methods return None
rather than self
. One possibility is to design your class from the beginning with method chaining in mind, but what do you do with those like the built-ins which aren't?
This is sometimes called method cascading. Here's a proof-of-concept for an adapter class which turns any object into one with methods that can be chained.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | try:
callable
except NameError:
# Python 3.0 or 3.1, sigh.
def callable(obj):
return hasattr(type(obj), '__call__')
class chained:
def __init__(self, obj):
self.obj = obj
def __repr__(self):
return repr(self.obj)
def __getattr__(self, name):
attr = getattr(self.obj, name)
if callable(attr):
def selfie(*args, **kw):
# Call the method just for side-effects, return self.
_ = attr(*args, **kw)
return self
return selfie
else:
return attr
|
This has been tested with Python versions 2.4 through 2.7, 3.2 and 3.3, plus Jython 2.5 and IronPython 2.6, but should work with just about any version of Python since closures where introduced (version 2.2, if I recall correctly).
The basic idea is that the chained
class delegates attribute look-ups to the wrapped object. If the attribute is callable, it's wrapped in a closure, selfie
, which calls the original attribute, discards whatever it returns (None
in the case of mutator methods), then returns self
, which is the adapter instance. Usage is simple, needing only a single extra call to wrap the original object before calling the cascade of methods:
x = []
chained(x).append(1).append(2).append(3).reverse().append(4)
There are a couple of minor limitations to this idea. Obviously you can only chain method calls, not data attributes. Special dunder (Double UNDERscore) methods like __repr__ aren't delegated in Python 3.x unless you manually add them to the chained
class, which means that operators won't behave correctly in Python 3. They also won't work correctly in Python 2, but for completely different reasons! Because the class is a "classic class" in Python 2, dunder methods are delegated, but operators like + sometimes require the method to return a specific type of value, which the wrapped methods don't do. So, Python 2 or 3, don't expect operators or anything that calls a dunder method to work correctly with the class as given. Finally, there is a clash between the adapter class and it's "obj" attribute, and any "obj" attribute the adapted class may have.
Despite these limitations, this is a quick, simple and effective way to get method chaining/cascading.
I think this style is commonly known as a "fluent API", in case you want to add the word "fluent" somewhere here so it turns up in search results: https://en.wikipedia.org/wiki/Fluent_interface