Basic method chaining

Question:

I found this 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.

Asked By: Pezze

||

Answers:

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.

Answered By: Eli Korvigo

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'
Answered By: Robᵩ

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.

Answered By: Lalit Vavdara
b = foo()
(
   b.line()
    .my_print()
    .bar()
    .my_print()
)
Answered By: Ima Sata
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.