Basic method chaining

PythonMethod Chaining

Python Problem Overview


I found this https://stackoverflow.com/questions/12172934/method-chaining-in-python, but even with it I couldn't understand method chaining in Python.

Here the goals are two: solve the coding problem and understand method chaining (given that I am still not 100% confident with callables).

Down to the problem definition.

I want a class that has two methods: one sets a parameter of the object = 'line' and the other overwrites to 'bar'.

This is what I got so far:

class foo():
    def __init__(self, kind=None):
        self.kind = kind

    def __call__(self, kind=None):
        return foo(kind=kind)

    def my_print(self):
        print (self.kind)

    def line(self):
        return self(kind='line')
    def bar(self):
        return self(kind='bar')

Sadly, with this code I can achieve my goal doing this

a = foo()
a.bar().line().bar().bar().line().my_print()

But I would like to obtain the same result by writing this code

a = foo()
a.bar.line.bar.bar.line.my_print()

How do I achieve this? I guess is something wrong in how I defined the __call__ method. Thanks in advance for your help.

Python Solutions


Solution 1 - Python

Method chaining is simply being able to add .second_func() to whatever .first_func() returns. It is fairly easily implemented by ensuring that all chainable methods return self. (Note that this has nothing to do with __call()__).

class foo():
    def __init__(self, kind=None):
        self.kind = kind
    def my_print(self):
        print (self.kind)
        return self
    def line(self):
        self.kind = 'line'
        return self
    def bar(self):
        self.kind='bar'
        return self

You can use foo objects in a non-chained way by ignoring their returned values:

a = foo()
a.line()
a.my_print()
a.bar()
a.my_print()

assert a.kind == 'bar'

Or, since every function now returns the object itself, you can operate directly on the returned value. You can use method chaining with this equivalent code:

b = foo()
b.line().my_print().bar().my_print()
assert b.kind == 'bar'

Or even:

c = foo().line().my_print().bar().my_print()
assert c.kind == 'bar'

The question of getting rid of the () calling syntax is a completely separate concept from method chaining. If you want chain properties, and have those properties mutate their object, use the @property decorator. (But mutating objects via a property seems dangerous. Better to use a method and name it with a verb: .set_line() instead of .line, for example.)

class foo():
    def __init__(self, kind=None):
        self.kind = kind
    def my_print(self):
        print (self.kind)
        return self
    @property
    def line(self):
        self.kind = 'line'
        return self
    @property
    def bar(self):
        self.kind='bar'
        return self

a = foo()
a.line
a.my_print()
a.bar
a.my_print()

assert a.kind == 'bar'

b = foo()
b.line.my_print().bar.my_print()
assert b.kind == 'bar'

c = foo().line.my_print().bar.my_print()
assert c.kind == 'bar'

Solution 2 - Python

Use properties (descriptors).

class foo:
    def __init__(self, kind=None):
        self.kind = kind

    def __call__(self, kind=None):
        return foo(kind=kind)

    def my_print(self):
        print (self.kind)

    @property
    def line(self):
        return self(kind='line')
    
    @property
    def bar(self):
        return self(kind='bar')

Note, though, that you overwrite nothing, the modification doesn't work inplace (which is arguably good, btw). Anyway, this doesn't look like a good design choice for most real-world cases, because at some point your methods will require arguments.

Solution 3 - Python

ther's an another interesting way of achieving this

class Foo:
    def __init__(self, kind=[]):
        self.kind = kind

    def __getattr__(self, attrs):
        self.attrs = attrs
        return Foo(self.kind + [attrs]) 

    def __call__(self):
        return self.kind[::-1][0]


my_obj = Foo()
print(my_obj.bar.line.bar.bar.line())

with this code u don't have to pass .my_print() but one thing to note here is the Foo class will take anything as argument like if we try print(my_obj.bar.line.bar.bar.circle()) it will return circle.

You can also edit this code to take the args while calling any function.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPezzeView Question on Stackoverflow
Solution 1 - PythonRobᵩView Answer on Stackoverflow
Solution 2 - PythonEli KorvigoView Answer on Stackoverflow
Solution 3 - PythonLalit VavdaraView Answer on Stackoverflow