Method chaining

Rotwang sg552 at hotmail.co.uk
Sat Nov 23 14:53:32 EST 2013


On 22/11/2013 11:26, Steven D'Aprano wrote:
> A frequently missed feature is the ability to chain method calls:
>
> x = []
> x.append(1).append(2).append(3).reverse().append(4)
> => x now equals [3, 2, 1, 4]
>
>
> This doesn't work with lists, as the methods return None rather than
> self. The class needs to be designed with method chaining in mind before
> it will work, and most Python classes follow the lead of built-ins like
> list and have mutator methods return None rather than self.
>
> Here's a proof-of-concept recipe to adapt any object so that it can be
> used for chaining method calls:
>
>
> class chained:
>      def __init__(self, obj):
>          self.obj = obj
>      def __repr__(self):
>          return repr(self.obj)
>      def __getattr__(self, name):
>          obj = getattr(self.obj, name)
>          if callable(obj):
>              def selfie(*args, **kw):
>                  # Call the method just for side-effects, return self.
>                  _ = obj(*args, **kw)
>                  return self
>              return selfie
>          else:
>              return obj
>
>
> chained([]).append(1).append(2).append(3).reverse().append(4)
> => returns [3, 2, 1, 4]

That's pretty cool. However, I can imagine it would be nice for the 
chained object to still be an instance of its original type. How about 
something like this:

def getr(self, name):
     obj = super(type(self), self).__getattribute__(name)
     if callable(obj):
         def selfie(*args, **kwargs):
             result = obj(*args, **kwargs)
             return self if result is None else result
         return selfie
     return obj

class chained(type):
     typedict = {}
     def __new__(cls, obj):
         if type(obj) not in cls.typedict:
             cls.typedict[type(obj)]  = type.__new__(
                 cls, 'chained%s' % type(obj).__name__,
                 (type(obj),), {'__getattribute__': getr})
         return cls.typedict[type(obj)](obj)


# In the interactive interpreter:
 >>> d = chained({}).update({1: 2}).update({3: 4})
 >>> d
{1: 2, 3: 4}
 >>> type(d)
<class '__main__.chaineddict'>
 >>> isinstance(d, dict)
True


The above code isn't very good - it will only work on types whose 
constructor will copy an instance, and it discards the original. And its 
dir() is useless. Can anyone suggest something better?



More information about the Python-list mailing list