Method chaining

S

Steven D'Aprano

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/
 
C

Chris Angelico

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
 
P

Peter Otten

Steven said:
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.
 
T

Terry Reedy

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.
 
S

Steven D'Aprano

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()
 
S

Steven D'Aprano

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.
 
P

Peter Otten

Steven said:
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.
 
A

Antoon Pardon

Op 22-11-13 16:20, Peter Otten schreef:
That cuts both ways ;)


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.
 
S

Steven D'Aprano

That cuts both ways ;)

Actually, I was wrong. See below.
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.

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.
 
W

Wolfgang Maier

Steven D'Aprano said:
# 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
 
W

Wolfgang Maier

Wolfgang Maier said:
Steven D'Aprano said:
# 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.
 
R

Rotwang

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: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?
 
R

Rotwang

[...]

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).
 
R

Rotwang

[...]

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.
 
S

Steven D'Aprano

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.
 
R

Rotwang

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

looks nicer than

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.



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)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,743
Messages
2,569,478
Members
44,899
Latest member
RodneyMcAu

Latest Threads

Top