Method chaining

Rotwang sg552 at hotmail.co.uk
Sun Nov 24 16:01:55 CET 2013


On 24/11/2013 14:27, Steven D'Aprano wrote:
> On Sat, 23 Nov 2013 19:53:32 +0000, Rotwang wrote:
>
>> On 22/11/2013 11:26, Steven D'Aprano wrote:
>>> A frequently missed feature is the ability to chain method calls:
> [...]
>>> 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.
>
> Why?

Well, if one intends to pass such an object to a function that does 
something like this:

def f(obj):
     if isinstance(obj, class1):
         do_something(obj)
     elif isinstance(obj, class2):
         do_something_else(obj)

then

 >>> f(chained(obj).method1().method2())

looks nicer than

 >>> f(chained(obj).method1().method2().obj)

and usually still works with the version of chained I posted last night. 
This isn't a wholly hypothetical example, I have functions in my own 
software that perform instance checks and that I often want to pass 
objects that I've just mutated several times.


> During the chained call, you're only working with the object's own
> methods, so that shouldn't matter. Even if a method calls an external
> function with self as an argument, and the external function insists on
> the original type, that doesn't matter because the method sees only the
> original (wrapped) object, not the chained object itself.
>
> In other words, in the example above, each of the calls to list.append
> etc. see only the original list, not the chained object.
>
> The only time you might care about getting the unchained object is at the
> end of the chain. This is where Ruby has an advantage, the base class of
> everything has a method useful for chaining methods, Object.tap, where in
> Python you either keep working with the chained() wrapper object, or you
> can extract the original and discard the wrapper.
>
>
>> How about something like this:
>>
>> def getr(self, name):
>>       obj = super(type(self), self).__getattribute__(name)
>
> I don't believe you can call super like that. I believe it breaks when
> you subclass the subclass.

Yes. I don't know what I was thinking with the various super calls I 
wrote last night, apart from being buggy they're completely unnecessary. 
Here's a better version:

class dummy:
     pass

def initr(self, obj):
     object.__setattr__(self, '__obj', obj)
def getr(self, name):
     try:
         return object.__getattribute__(self, name)
     except AttributeError:
         return getattr(self.__obj, name)
def methr(method):
     def cmethod(*args, **kwargs):
         try:
             args = list(args)
             self = args[0]
             args[0] = self.__obj
         except (IndexError, AttributeError):
             self = None
         result = method(*args, **kwargs)
         return self if result is None else result
     try:
         cmethod.__qualname__ = method.__qualname__
     except AttributeError:
         pass
     return cmethod

class chained(type):
     typedict = {}
     def __new__(cls, obj):
         if isinstance(type(obj), chained):
             return obj
         if type(obj) not in cls.typedict:
             dict = {}
             for t in reversed(type(obj).__mro__):
                 dict.update({k: methr(v) for k, v in t.__dict__.items()
                             if callable(v) and k != '__new__'})
             dict.update({'__init__': initr, '__getattribute__': getr})
             cls.typedict[type(obj)] = type.__new__(cls, 'chained%s'
                 % type(obj).__name__, (dummy, type(obj)), dict)
         return cls.typedict[type(obj)](obj)



More information about the Python-list mailing list