FAQ
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

Search Discussions

  • Chris Angelico at Nov 22, 2013 at 11:44 am

    On Fri, Nov 22, 2013 at 10:26 PM, Steven D'Aprano 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
  • Peter Otten at Nov 22, 2013 at 12:08 pm

    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.
  • Terry Reedy at Nov 22, 2013 at 12:34 pm

    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
  • Steven D'Aprano at Nov 22, 2013 at 2:26 pm

    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
  • Peter Otten at Nov 22, 2013 at 3:20 pm

    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.
  • Antoon Pardon at Nov 22, 2013 at 3:38 pm

    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.
  • Laszlo Nagy at Nov 23, 2013 at 4:08 pm

    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.
  • Steven D'Aprano at Nov 22, 2013 at 4:26 pm

    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
  • Wolfgang Maier at Nov 22, 2013 at 4:52 pm

    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
  • Wolfgang Maier at Nov 22, 2013 at 5:55 pm
    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.
  • Steven D'Aprano at Nov 22, 2013 at 2:30 pm

    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
  • Rotwang at Nov 23, 2013 at 7:53 pm

    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?
  • Rotwang at Nov 24, 2013 at 12:28 am

    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).
  • Rotwang at Nov 24, 2013 at 12:43 am

    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.
  • Steven D'Aprano at Nov 24, 2013 at 2:27 pm

    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
  • Rotwang at Nov 24, 2013 at 3:01 pm

    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)

Related Discussions

Discussion Navigation
viewthread | post
Discussion Overview
grouppython-list @
categoriespython
postedNov 22, '13 at 11:26a
activeNov 24, '13 at 3:01p
posts17
users8
websitepython.org

People

Translate

site design / logo © 2022 Grokbase