Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.python > #60213 > unrolled thread

Method chaining

Started bySteven D'Aprano <steve+comp.lang.python@pearwood.info>
First post2013-11-22 11:26 +0000
Last post2013-11-24 15:01 +0000
Articles 17 — 8 participants

Back to article view | Back to comp.lang.python


Contents

  Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 11:26 +0000
    Re: Method chaining Chris Angelico <rosuav@gmail.com> - 2013-11-22 22:44 +1100
    Re: Method chaining Peter Otten <__peter__@web.de> - 2013-11-22 13:08 +0100
      Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 14:26 +0000
        Re: Method chaining Peter Otten <__peter__@web.de> - 2013-11-22 16:20 +0100
          Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 16:26 +0000
            Re: Method chaining Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> - 2013-11-22 16:52 +0000
            Re: Method chaining Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> - 2013-11-22 17:55 +0000
        Re: Method chaining Antoon Pardon <antoon.pardon@rece.vub.ac.be> - 2013-11-22 16:38 +0100
        Re: Method chaining Laszlo Nagy <gandalf@shopzeus.com> - 2013-11-23 17:08 +0100
    Re: Method chaining Terry Reedy <tjreedy@udel.edu> - 2013-11-22 07:34 -0500
      Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-22 14:30 +0000
    Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-23 19:53 +0000
      Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-24 00:28 +0000
        Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-24 00:43 +0000
      Re: Method chaining Steven D'Aprano <steve+comp.lang.python@pearwood.info> - 2013-11-24 14:27 +0000
        Re: Method chaining Rotwang <sg552@hotmail.co.uk> - 2013-11-24 15:01 +0000

#60213 — Method chaining

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2013-11-22 11:26 +0000
SubjectMethod chaining
Message-ID<528f3f4e$0$29992$c3e8da3$5496439d@news.astraweb.com>
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]


Tested, and works, in CPython 2.4 through 2.7, 3.2 and 3.3, Jython 2.5, 
and IronPython 2.6.

See here for further discussion of the limitations:

http://code.activestate.com/recipes/578770-method-chaining/



-- 
Steven

[toc] | [next] | [standalone]


#60214

FromChris Angelico <rosuav@gmail.com>
Date2013-11-22 22:44 +1100
Message-ID<mailman.3033.1385120652.18130.python-list@python.org>
In reply to#60213
On Fri, Nov 22, 2013 at 10:26 PM, Steven D'Aprano
<steve+comp.lang.python@pearwood.info> wrote:
>         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

Nice piece of magic. One limitation not mentioned is that this
completely destroys the chance to have a method return anything _other
than_ self. Since this is intended for Python's convention of
"mutators return None", I'd be inclined to check for a None return,
though that might still have some false positives.

            def selfie(*args, **kw):
                # Call the method for side-effects, return self if it
returns None.
                _ = obj(*args, **kw)
                if _ is None: return self
                return _
            return selfie

Either that, or manually identify a set of methods to wrap, which
could possibly be done fairly cleanly with a list of names passed to
__init__. That'd be more work, though.

ChrisA

[toc] | [prev] | [next] | [standalone]


#60216

FromPeter Otten <__peter__@web.de>
Date2013-11-22 13:08 +0100
Message-ID<mailman.3035.1385122060.18130.python-list@python.org>
In reply to#60213
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]
> 
> 
> Tested, and works, in CPython 2.4 through 2.7, 3.2 and 3.3, Jython 2.5,
> and IronPython 2.6.
> 
> See here for further discussion of the limitations:
> 
> http://code.activestate.com/recipes/578770-method-chaining/

Here's my take:

class Chained(object):
    def __init__(self, value, method=None):
        self.value = value
        self.method = method
    def __call__(self, *args, **kw):
        result = self.method(*args, **kw)
        if result is None:
            result = self.value
        return Chained(result)
    def __getattr__(self, name):
        return Chained(self.value, getattr(self.value, name))

if __name__ == "__main__":
    print(Chained([]).append(1).append(2).append(3).reverse().append(4).value)
    print(Chained([]).append(1).extend([2,1,1]).count(1).value)

These things are nice to write as long as you omit the gory details, but 
personally I don't want to see the style it favours in my or other people's 
code.

[toc] | [prev] | [next] | [standalone]


#60228

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2013-11-22 14:26 +0000
Message-ID<528f6989$0$29992$c3e8da3$5496439d@news.astraweb.com>
In reply to#60216
On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:

> These things are nice to write as long as you omit the gory details, but
> personally I don't want to see the style it favours in my or other
> people's code.

There's not really a lot of difference between:

    obj = MyClass()
    obj.spam()
    obj.eggs()
    obj.cheese()

and

    obj = MyClass().spam().eggs().cheese()


except the first takes up a lot more vertical space. Chained method calls 
is idiomatic in some languages. If there is a problem with it, it is that 
it doesn't make it clear that each method call is being used only for its 
side-effects, rather than it being a series of distinct objects. But in 
my opinion that flaw is a very minor one.

The nice thing about using an explicit method chaining call rather than 
building your class to support it by default is that the initial call to 
the adaptor signals that everything that follows is called only for the 
side-effects.

    obj = chained(MyClass()).spam().eggs().cheese()



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#60235

FromPeter Otten <__peter__@web.de>
Date2013-11-22 16:20 +0100
Message-ID<mailman.3044.1385133587.18130.python-list@python.org>
In reply to#60228
Steven D'Aprano wrote:

> On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:
> 
>> These things are nice to write as long as you omit the gory details, but
>> personally I don't want to see the style it favours in my or other
>> people's code.
> 
> There's not really a lot of difference 

That cuts both ways ;)

> between:
> 
>     obj = MyClass()
>     obj.spam()
>     obj.eggs()
>     obj.cheese()
> 
> and
> 
>     obj = MyClass().spam().eggs().cheese()
> 
> 
> except the first takes up a lot more vertical space. 

I've not yet run short of vertical space ;)

> Chained method calls is idiomatic in some languages. 

Languages with mutable objects?

> If there is a problem with it, it is that
> it doesn't make it clear that each method call is being used only for its
> side-effects, rather than it being a series of distinct objects. But in
> my opinion that flaw is a very minor one.
> 
> The nice thing about using an explicit method chaining call rather than
> building your class to support it by default is that the initial call to
> the adaptor signals that everything that follows is called only for the
> side-effects.
> 
>     obj = chained(MyClass()).spam().eggs().cheese()

      obj = MyClass(); obj.spam(); obj.eggs(); obj.cheese()

OK, that one is disgusting...

Anyway, I'd like to see a sequence of method names taken from actual code 
that profits from this chaining pattern.

[toc] | [prev] | [next] | [standalone]


#60239

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2013-11-22 16:26 +0000
Message-ID<528f859e$0$29992$c3e8da3$5496439d@news.astraweb.com>
In reply to#60235
On Fri, 22 Nov 2013 16:20:03 +0100, Peter Otten wrote:

> Steven D'Aprano wrote:
> 
>> On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:
>> 
>>> These things are nice to write as long as you omit the gory details,
>>> but personally I don't want to see the style it favours in my or other
>>> people's code.
>> 
>> There's not really a lot of difference
> 
> That cuts both ways ;)

Actually, I was wrong. See below.

>> between:
>> 
>>     obj = MyClass()
>>     obj.spam()
>>     obj.eggs()
>>     obj.cheese()
>> 
>> and
>> 
>>     obj = MyClass().spam().eggs().cheese()
>> 
>> 
>> except the first takes up a lot more vertical space.
> 
> I've not yet run short of vertical space ;)

However, here is a real difference:

# With chaining
thing = func(MyClass().spam().eggs().cheese(), 
             MyClass().aardvark(),
             OtherClass().fe().fi().fo().fum(),
             )
do_stuff_with(thing)


versus:

# Without chaining
temp1 = MyClass()
temp1.spam()
temp1.eggs()
temp1.cheese()
temp2 = MyClass()
temp2.aardvark()
temp3 = OtherClass()
temp3.fe()
temp3.fi()
temp3.fo()
temp3.fum()
thing = func(temp1, temp2, temp3)
do_stuff_with(thing)


In this case the chained version doesn't obscure the intention of the 
code anywhere near as much as the unchained version and its plethora of 
temporary variables.


>> Chained method calls is idiomatic in some languages.
> 
> Languages with mutable objects?

Yes. It's a "Design Pattern" applicable to any language with mutator 
methods. Here are three examples in C#, Java and Ruby:

http://mrbool.com/fluent-interface-and-method-chaining-a-good-programming-approach/26365
http://www.infoq.com/articles/internal-dsls-java
http://blog.jayfields.com/2008/03/ruby-replace-temp-with-chain.html

although in fairness I wouldn't call it idiomatic in C# or Java.

Ruby 1.9 even added a new method to Object, tap, specifically to allow 
chaining of methods, which itself was copied from Ruby-On-Rails' 
"returning" helper. So I think it's fair to say that method chaining
for mutation is idiomatic in Ruby.

http://www.seejohncode.com/2012/01/02/ruby-tap-that/

http://blog.moertel.com/posts/2007-02-07-ruby-1-9-gets-handy-new-method-object-tap.html


This idea goes back to Smalltalk, and is essentially just a 
pipeline. Hardly something weird.

http://en.wikipedia.org/wiki/Method_chaining

(Technically, if the methods return self rather than None, it's method 
cascading. Chaining is any sequence of method calls, whether they return
self or something else.)

Dart includes syntax for method cascades, which I'm told looks like 
this:

x = SomeClass()
    ..spam()
    ..eggs()
    ..cheese()


The technique also comes with the blessing of Martin Fowler, where it
is an important part of fluent interfaces:

http://martinfowler.com/bliki/FluentInterface.html

Quote:

"The common convention in the curly brace world is that modifier 
methods are void, which I like because it follows the principle 
of CommandQuerySeparation. This convention does get in the way of 
a fluent interface, so I'm inclined to suspend the convention for 
this case."


And last but not least, if you want to go all the way down to the lambda 
calculus and combinator theory, my "selfie" adapter function is just a 
form of the K-combinator (a.k.a. the Kestrel). So there's a deep and 
powerful mathematical pedigree to the idea, if that matters.



-- 
Steven

[toc] | [prev] | [next] | [standalone]


#60243

FromWolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de>
Date2013-11-22 16:52 +0000
Message-ID<mailman.3048.1385139196.18130.python-list@python.org>
In reply to#60239
Steven D'Aprano <steve+comp.lang.python <at> pearwood.info> writes:

> # With chaining
> thing = func(MyClass().spam().eggs().cheese(), 
>              MyClass().aardvark(),
>              OtherClass().fe().fi().fo().fum(),
>              )
> do_stuff_with(thing)
> 
> versus:
> 
> # Without chaining
> temp1 = MyClass()
> temp1.spam()
> temp1.eggs()
> temp1.cheese()
> temp2 = MyClass()
> temp2.aardvark()
> temp3 = OtherClass()
> temp3.fe()
> temp3.fi()
> temp3.fo()
> temp3.fum()
> thing = func(temp1, temp2, temp3)
> do_stuff_with(thing)
> 

Another use case might be in comprehensions and generator expressions ??

thing = [MyClass().spam().eggs(x).cheese() for x in sequence]

where you can't use all those temporary assignments.

Best,
Wolfgang

[toc] | [prev] | [next] | [standalone]


#60246

FromWolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de>
Date2013-11-22 17:55 +0000
Message-ID<mailman.3051.1385142973.18130.python-list@python.org>
In reply to#60239
Wolfgang Maier <wolfgang.maier <at> biologie.uni-freiburg.de> writes:

> 
> Steven D'Aprano <steve+comp.lang.python <at> pearwood.info> writes:
> 
> > # With chaining
> > thing = func(MyClass().spam().eggs().cheese(), 
> >              MyClass().aardvark(),
> >              OtherClass().fe().fi().fo().fum(),
> >              )
> > do_stuff_with(thing)
> > 
> > versus:
> > 
> > # Without chaining
> > temp1 = MyClass()
> > temp1.spam()
> > temp1.eggs()
> > temp1.cheese()
> > temp2 = MyClass()
> > temp2.aardvark()
> > temp3 = OtherClass()
> > temp3.fe()
> > temp3.fi()
> > temp3.fo()
> > temp3.fum()
> > thing = func(temp1, temp2, temp3)
> > do_stuff_with(thing)
> > 
> 
> Another use case might be in comprehensions and generator expressions ??
> 
> thing = [MyClass().spam().eggs(x).cheese() for x in sequence]
> 
> where you can't use all those temporary assignments.
> 
> Best,
> Wolfgang
> 
> 

Thinking about this, you could define __call__ for your chained class to
return the wrapped object:

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
    def __call__(self):
        return self.obj

This would encourage its localized use on the fly as in:

thing = [MyClass().spam().eggs(x).cheese()() for x in sequence]

where the intention probably would be to generate a list of lists (or other
mutable objects), not of objects of class chained. Localizing the use of
chained also seems like a good idea to me as I do share Peter's worries
about coding style, while I also agree with you that such a class comes in
handy from time to time.

[toc] | [prev] | [next] | [standalone]


#60237

FromAntoon Pardon <antoon.pardon@rece.vub.ac.be>
Date2013-11-22 16:38 +0100
Message-ID<mailman.3045.1385134715.18130.python-list@python.org>
In reply to#60228
Op 22-11-13 16:20, Peter Otten schreef:
> Steven D'Aprano wrote:
> 
>> On Fri, 22 Nov 2013 13:08:03 +0100, Peter Otten wrote:
>>
>>> These things are nice to write as long as you omit the gory details, but
>>> personally I don't want to see the style it favours in my or other
>>> people's code.
>>
>> There's not really a lot of difference 
> 
> That cuts both ways ;)
> 
>> between:
>>
>>     obj = MyClass()
>>     obj.spam()
>>     obj.eggs()
>>     obj.cheese()
>>
>> and
>>
>>     obj = MyClass().spam().eggs().cheese()
>>
>>
>> except the first takes up a lot more vertical space. 
> 
> I've not yet run short of vertical space ;)

Really? Then you must write only very short programs. Me I
continuously run out of vertical space. That is why I need
to use such tools as scroll bars.

-- 
Antoon Pardon.

[toc] | [prev] | [next] | [standalone]


#60309

FromLaszlo Nagy <gandalf@shopzeus.com>
Date2013-11-23 17:08 +0100
Message-ID<mailman.3091.1385222894.18130.python-list@python.org>
In reply to#60228
>
> OK, that one is disgusting...
>
> Anyway, I'd like to see a sequence of method names taken from actual code
> that profits from this chaining pattern.
>
Actually, wx.lib.agw uses this a lot. Especially for AuiPaneInfo:

http://www.wxpython.org/docs/api/wx.aui.AuiPaneInfo-class.html

All right, this is a C++ proxy. But still, it is actively used by many.

-- 
This message has been scanned for viruses and
dangerous content by MailScanner, and is
believed to be clean.

[toc] | [prev] | [next] | [standalone]


#60217

FromTerry Reedy <tjreedy@udel.edu>
Date2013-11-22 07:34 -0500
Message-ID<mailman.3036.1385123714.18130.python-list@python.org>
In reply to#60213
On 11/22/2013 6:26 AM, 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

True for the 7 pure mutation methods but not for .copy, .count, .index, 
and .pop. The last both mutates and returns.

-- 
Terry Jan Reedy

[toc] | [prev] | [next] | [standalone]


#60229

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2013-11-22 14:30 +0000
Message-ID<528f6a7f$0$29992$c3e8da3$5496439d@news.astraweb.com>
In reply to#60217
On Fri, 22 Nov 2013 07:34:53 -0500, Terry Reedy wrote:

> On 11/22/2013 6:26 AM, 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
> 
> True for the 7 pure mutation methods but not for .copy, .count, .index,
> and .pop. The last both mutates and returns.

Yes, that is correct. In this case, the assumption behind the chained 
adapter is that we don't care about the results of calling those methods, 
we only care about the mutation they cause. If that's not the case, then 
chained() isn't for us.


-- 
Steven

[toc] | [prev] | [next] | [standalone]


#60318

FromRotwang <sg552@hotmail.co.uk>
Date2013-11-23 19:53 +0000
Message-ID<l6r13t$ioa$1@dont-email.me>
In reply to#60213
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?

[toc] | [prev] | [next] | [standalone]


#60342

FromRotwang <sg552@hotmail.co.uk>
Date2013-11-24 00:28 +0000
Message-ID<l6rh76$hj9$1@dont-email.me>
In reply to#60318
On 23/11/2013 19:53, Rotwang wrote:
> [...]
>
> 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:
>
> [crap code]
>
> 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?

Here's another attempt:

class dummy:
     pass

def initr(self, obj):
     super(type(self), self).__setattr__('__obj', obj)
def getr(self, name):
     try:
         return super(type(self), self).__getattribute__(name)
     except AttributeError:
         return getattr(self.__obj, name)
def methr(method):
     def selfie(self, *args, **kwargs):
         result = method(self.__obj, *args, **kwargs)
         return self if result is None else result
     return selfie

class chained(type):
     typedict = {}
     def __new__(cls, 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)


This solves some of the problems in my earlier effort. It keeps a copy 
of the original object, while leaving its interface pretty much 
unchanged; e.g. repr does what it's supposed to, and getting or setting 
an attribute of the chained object gets or sets the corresponding 
attribute of the original. It won't work on classes with properties, 
though, nor on classes with callable attributes that aren't methods (for 
example, a class with an attribute which is another class).

[toc] | [prev] | [next] | [standalone]


#60343

FromRotwang <sg552@hotmail.co.uk>
Date2013-11-24 00:43 +0000
Message-ID<l6ri47$ljb$1@dont-email.me>
In reply to#60342
On 24/11/2013 00:28, Rotwang wrote:
> [...]
>
> This solves some of the problems in my earlier effort. It keeps a copy
> of the original object,

Sorry, I meant that it keeps a reference to the original object.

[toc] | [prev] | [next] | [standalone]


#60375

FromSteven D'Aprano <steve+comp.lang.python@pearwood.info>
Date2013-11-24 14:27 +0000
Message-ID<52920cde$0$29993$c3e8da3$5496439d@news.astraweb.com>
In reply to#60318
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? 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.


-- 
Steven

[toc] | [prev] | [next] | [standalone]


#60383

FromRotwang <sg552@hotmail.co.uk>
Date2013-11-24 15:01 +0000
Message-ID<l6t4d3$fvs$1@dont-email.me>
In reply to#60375
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)

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.python


csiph-web