Fluent interface with Python
Asked Answered
S

2

9

I have a Python function "send_message" which takes three arguments:

send_message("i like windmills", to="INBOX", from="OUTBOX")

I am thinking about putting a fluent interface on top of it. Ideally I'd like to write any of the following:

send_message("i like windmills").to("INBOX").from("OUTBOX")

send_message("i like windmills").from("OUTBOX").to("INBOX")

# The `to()` information is mandatory but the `from()` is not (as with real letters), so this one would also be a valid call:
send_message("i like windmills").to("INBOX")

Any ideas how to accomplish this or something similar?

The general approach of having methods of an object returning "self" is understood by me but in my understanding this would lead to something like this:

message = Message("i like windmills")
message.to("INBOX").from("OUTBOX").send()

But this one is not as nice as the previous example and I then would actually prefer the original version with the named arguments.

Any help is appreciated.

Smokedry answered 15/6, 2016 at 6:38 Comment(8)
You want to implement the Builder Pattern. send_message would be the factory method to create the builder.Hamper
Alternatively you could implement currying; it avoids the object "overhead" which apparently is not desired by the OP.Marks
Hi Lutz, not quite. The builder pattern requires a last "build()" method in the chain to actually perform the action. I was hoping to find a way that each method in the chain can tell if it is the last one (by meta programming?). If this is the case it actually performs the action implicitly.Smokedry
@samba2: then please edit into your question how this is different to Builder Pattern. I don't understand how message.to("INBOX").from("OUTBOX").send() is "not as nice" as the previous example, I think you mean there shouldn't be a trailing /build()/send()/whatever() call, and each method call should automagically figure out if it's the last in the chain, and if yes trigger a .send(). That sounds undesirable and risky to me, because now you can't do multiple assignments like general_msg = send_message("i like windmills").from("OUTBOX") and specific_msg = general_msg.to("Shirley")..Docia
...without the first assignment triggering a widespread .send() to all. And it forces all your fluent lines to be one line with unlimited line-length, can't split complicated code across lines.Docia
I do not believe this is "fluent interface", it sounds like "magic self-building fluent interface". Sending unwanted messages is not disastrously bad, but just imagine writing this sort of code on bank accounts...Docia
Some criticisms of what the method-chain is ultimately supposed to do/return DaveGlick, 2014: "Method Chaining, Fluent Interfaces, and the Finishing Problem Or Why You Can't Have Your Cake And Eat It Too". Fluent is/was a fad that is great for some domains (e.g. UI), and disastrous for others, whenever a control flow exception or need to rewind/ undo a previous call would be a problem.Docia
The syntax design of python is quite bad. I suspect that the author’s mother tongue is written from right to left.Caz
C
8

It can be accomplished this way, I am unsure if there is a better way because this is my first attempt. Good luck!

DEFAULT_SENDER = 'my_address'
#Because the sender object is optional I assume you have a default sender

class Send_message(object):
    def __init__(self, message):
        self.message = message
        self.sender = None
        self.receiver = None
        self.method = None

    def to(self, receiver):
        self.receiver = receiver
        self.method = self.send()
        return self

    def _from(self, sender):
        self.sender = sender
        self.method = self.send()
        return self

    def __call__(self):
        if self.method:
            return self.method()
        return None

    def send(self):
        if self.receiver:
            if not self.sender:
                self.sender = DEFAULT_SENDER

            return lambda:actual_message_code(self.message, self.sender, self.receiver)


def actual_message_code(message, sender, receiver):
    print "Sent '{}' from: {} to {}.".format(message, sender, receiver)



Send_message("Hello")._from('TheLazyScripter').to('samba2')()
Send_message("Hello").to('samba2')._from('TheLazyScripter')()
Send_message("Hello").to('samba2')()

#Only change in actual calling is the trailing ()

By implementing the __call__ method we can tell the when we are at the end of the call chain. This of course adds the trailing () call. and requires you to change the pointer to your actual messaging method and default sender variable but I feel that this would be the simplest way to accomplish your goals without actually knowing when the chain ends.

Chopper answered 15/6, 2016 at 6:47 Comment(2)
Hi Lazy Scripter. I voted your answer up because it is a beautifully simple solution to my initial problem. However, as it goes with agile requirements. You pointed me to a fact which was missing in my original question: the to()method is mandatory whereas the from()method is optional. since your idea is based on the presents of the two properties "sender" and "receiver" I assume it won't work.Smokedry
I have updated my code to fit your updated parameters.Chopper
X
4

There is one drawback in returning self. Mutating one of the variables can affect another one. Here is an example

Taken from @TheLazyScripter code but with modified example.

a = Send_message("Hello")
b = a
a = a._from('theLazyscripter')
b = b._from('Kracekumar').to('samba 2')
b()
a.message = 'Hello A'
a.to('samba2')()
b.to('samba 2')()
Send_message("Hello").to('samba2')._from('TheLazyScripter')()
Send_message("Hello").to('samba2')()

The a and b variable points to the same instance. Modifying one's value will affect the other ones. See the second and third line of the output.

The output

Sent 'Hello' from: Kracekumar to samba 2.
Sent 'Hello A' from: Kracekumar to samba2.
Sent 'Hello A' from: Kracekumar to samba 2.
Sent 'Hello' from: TheLazyScripter to samba2.
Sent 'Hello' from: my_address to samba2.

b=a and modifying the a's content affects b's value.

How to remove this side-effect?

Rather than returning self return a new instance to remove side-effect.

DEFAULT_SENDER = 'my_address'
#Because the sender object is optional I assume you have a default sender

class Send_message(object):
    def __init__(self, message):
        self.message = message
        self.sender = None
        self.receiver = None
        self.method = None

    def _clone(self):
        inst = self.__class__(message=self.message)
        inst.sender = self.sender
        inst.receiver = self.receiver
        inst.method = self.method
        return inst

    def to(self, receiver):
        self.receiver = receiver
        self.method = self.send()
        return self._clone()

    def _from(self, sender):
        self.sender = sender
        self.method = self.send()
        return self._clone()

    def __call__(self):
        if self.method:
            return self.method()
        return None

    def send(self):
        if self.receiver:
            if not self.sender:
                self.sender = DEFAULT_SENDER

        return lambda:actual_message_code(self.message, self.sender, self.receiver)


def actual_message_code(message, sender, receiver):
    print("Sent '{}' from: {} to {}.".format(message, sender, receiver))



a = Send_message("Hello")
b = a
a = a._from('theLazyscripter')
b = b._from('Kracekumar').to('samba 2')
b()
a.message = 'Hello A'
a.to('samba2')()
b.to('samba 2')()
Send_message("Hello").to('samba2')._from('TheLazyScripter')()
Send_message("Hello").to('samba2')()

The _clone method creates a new copy of the instance every time. Note: when one of the values is a list or dictionary, the deep copy needs to be called. Here it's string hence not required. But the idea remains the same, copy each attribute before returning.

Output

Sent 'Hello' from: Kracekumar to samba 2.
Sent 'Hello A' from: theLazyscripter to samba2.
Sent 'Hello' from: Kracekumar to samba 2.
Sent 'Hello' from: TheLazyScripter to samba2.
Sent 'Hello' from: my_address to samba2.

The output line number 2 and 3 clearly shows the absence of side-effect in the new code.

I wrote a blog post about Fluent Interface

Xenophobia answered 6/6, 2021 at 21:15 Comment(1)
I agree with this strategy. The other would be considered 'unpythonic' by the creator of Python.Hearthstone

© 2022 - 2024 — McMap. All rights reserved.