29

(not to be confused with itertools.chain)

I was reading the following: http://en.wikipedia.org/wiki/Method_chaining

My question is: what is the best way to implement method chaining in python?

Here is my attempt:

class chain():
    def __init__(self, my_object):
        self.o = my_object

    def __getattr__(self, attr):
        x = getattr(self.o, attr)
        if hasattr(x, '__call__'):
            method = x
            return lambda *args: self if method(*args) is None else method(*args)
        else:
            prop = x
            return prop

list_ = chain([1, 2, 3, 0])
print list_.extend([9, 5]).sort().reverse()

"""
C:\Python27\python.exe C:/Users/Robert/PycharmProjects/contests/sof.py
[9, 5, 3, 2, 1, 0]
"""

One problem is if calling method(*args) modifies self.o but doesn't return None. (then should I return self or return what method(*args) returns).

Does anyone have better ways of implementing chaining? There are probably many ways to do it.

Should I just assume a method always returns None so I may always return self.o ?

Pierre GM
  • 19,809
  • 3
  • 56
  • 67
Rusty Rob
  • 16,489
  • 8
  • 100
  • 116
  • (note i'm not sure if method chaining should be used in python but i'm still interested) – Rusty Rob Aug 29 '12 at 07:35
  • You should use [pure functions](http://en.wikipedia.org/wiki/Pure_function) so that methods don't modify `self.o` directly, but instead return the modified version. Also, `__getattr__` should return a chain instance. – reubano Jan 30 '15 at 07:25

5 Answers5

26

There is a very handy Pipe library which may be the answer to your question. For example::

seq = fib() | take_while(lambda x: x < 1000000) \
            | where(lambda x: x % 2) \
            | select(lambda x: x * x) \
            | sum()
wjandrea
  • 28,235
  • 9
  • 60
  • 81
Zaur Nasibov
  • 22,280
  • 12
  • 56
  • 83
  • 3
    +1 After getting used to C#'s LINQ, `Pipe` is the first thing I import in Python. (You need to be careful with `from Pipe import *` though) – Kos Aug 29 '12 at 07:55
  • 5
    Anyway, `Pipe` is for composing functions - passing the result of function A as an argument for function B. Chaining is (usually) about returning the same object from several calls in order to do several modifications in one chain. JQuery has popularized that a lot. – Kos Aug 29 '12 at 08:01
  • 1
    True. But I believe chaining should not be used in Python the way it is used in JavaScript. – Zaur Nasibov Aug 29 '12 at 08:04
  • Thanks, I marked this as correct just because I learnt some cool stuff reading through the source code of pipe. – Rusty Rob Aug 29 '12 at 08:12
  • @robertking, @Kos, @BasicWolf Please see [my answer](http://stackoverflow.com/a/28232547/408556) below which is an basic and much simpler implementation of the `Pipe` lib. – reubano Jan 30 '15 at 09:17
  • There is also this library which is more feature rich (API + read/write data formats) and actively maintained github.com/EntilZha/ScalaFunctional – EntilZha Feb 13 '16 at 19:49
17

It's possible if you use only pure functions so that methods don't modify self.data directly, but instead return the modified version. You also have to return Chainable instances.

Here's an example using collection pipelining with lists:

import itertools

try:
    import builtins
except ImportError:
    import __builtin__ as builtins


class Chainable(object):
    def __init__(self, data, method=None):
        self.data = data
        self.method = method

    def __getattr__(self, name):
        try:
            method = getattr(self.data, name)
        except AttributeError:
            try:
                method = getattr(builtins, name)
            except AttributeError:
                method = getattr(itertools, name)

        return Chainable(self.data, method)

    def __call__(self, *args, **kwargs):
        try:
            return Chainable(list(self.method(self.data, *args, **kwargs)))
        except TypeError:
            return Chainable(list(self.method(args[0], self.data, **kwargs)))

Use it like this:

chainable_list = Chainable([3, 1, 2, 0])
(chainable_list
    .chain([11,8,6,7,9,4,5])
    .sorted()
    .reversed()
    .ifilter(lambda x: x%2)
    .islice(3)
    .data)
>> [11, 9, 7]

Note that .chain refers to itertools.chain and not the OP's chain.

reubano
  • 5,087
  • 1
  • 42
  • 41
11

Caveat: This only works on class methods() that do not intend to return any data.

I was looking for something similar for chaining Class functions and found no good answer, so here is what I did and thought was a very simple way of chaining: Simply return the self object.

So here is my setup:

class Car:
    def __init__(self, name=None):
        self.name = name
        self.mode = 'init'

    def set_name(self, name):
        self.name = name
        return self

    def drive(self):
        self.mode = 'drive'
        return self

And now I can name the car and put it in drive state by calling:

my_car = Car()
my_car.set_name('Porche').drive()

Hope this helps!

chris Frisina
  • 19,086
  • 22
  • 87
  • 167
Sarang
  • 2,143
  • 24
  • 21
  • yup that makes sense - I think chaining only makes sense on an object that controls its own state but exposes methods of interaction. Receiving data should usually be a seperate call after the process of modifying state, otherwise there's no clear abstraction – Rusty Rob Feb 29 '20 at 20:59
  • I guess there's also the "builder method". Not quite the same as CQRS. – Rusty Rob Jul 26 '22 at 19:38
4

There isn't going to be any general way of allowing any method of any object to be chained, since you can't know what sort of value that method returns and why without knowing how that particular method works. Methods might return None for any reason; it doesn't always mean the method has modified the object. Likewise, methods that do return a value still might not return a value that can be chained. There's no way to chain a method like list.index: fakeList.index(1).sort() can't have much hope of working, because the whole point of index is it returns a number, and that number means something, and can't be ignored just to chain on the original object.

If you're just fiddling around with Python's builtin types to chain certain specific methods (like sort and remove), you're better off just wrapping those particular methods explicitly (by overriding them in your wrapper class), instead of trying to do a general mechanism with __getattr__.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • Thanks - you're right. I didn't think it was possible to be general. The best I can think is to let the programmer decide if they want to chain or not. E.G. By default it doesn't return self (it just returns what the method returns) but if the programmer explicitly went list_.chain.sort() it might return list_ instead of None. – Rusty Rob Aug 29 '12 at 08:06
  • 3
    @robertking: Right, the general deal is that method chaining isn't something you can easily tack on to existing objects. You basically have to design a class with method chaining in mind for it to work properly. – BrenBarn Aug 29 '12 at 08:08
2

What about

def apply(data, *fns):
    return data.__class__(map(fns[-1], apply(data, *fns[:-1]))) if fns else data

>>> print(
...   apply(
...     [1,2,3], 
...     str, 
...     lambda x: {'1': 'one', '2': 'two', '3': 'three'}[x], 
...     str.upper))
['ONE', 'TWO', 'THREE']
>>> 

?

.. even keeps the type :)

frans
  • 8,868
  • 11
  • 58
  • 132