使用Python实现流畅接口

8
我有一个Python函数 "send_message",它需要三个参数:
send_message("i like windmills", to="INBOX", from="OUTBOX")

我正在考虑在此基础上添加流畅接口。理想情况下,我想要编写以下任意一种:

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")

有什么想法可以实现这个或类似的功能吗?

我理解对象方法返回“self”的一般方法,但在我看来,这会导致像这样的结果:

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

但这个例子不如前一个好,我更喜欢带有命名参数的原始版本。感谢您的任何帮助。

1
或者,你可以实现 柯里化;它避免了对象“开销”,这显然不是 OP 所期望的。 - Burhan Khalid
1
嗨,Lutz,不完全正确。建造者模式需要在链中有一个最后的“build()”方法来实际执行操作。我希望找到一种方法,使链中的每个方法都可以通过元编程告诉它是否是最后一个方法。如果是这种情况,它会隐式地执行操作。 - samba2
@samba2:然后请编辑您的问题,说明这与构建器模式有何不同。 我不明白message.to(“INBOX”).from(“OUTBOX”).send()比以前的示例不好,我认为您的意思是不应该有尾随的 /build()/send()/whatever(), 每个方法调用应该自动弄清楚它是否是链中的最后一个元素,如果是,则触发.send()。 对我来说,这听起来很不理想和冒险,因为现在您不能进行多个赋值,例如 general_msg = send_message("i like windmills").from("OUTBOX")specific_msg = general_msg.to("Shirley").. - smci
而不触发广泛的.send(),它强制所有流畅行都成为具有无限行长的一行,不能将复杂代码跨越几行。 - smci
一些批评方法链最终应该做什么/返回什么的声音DaveGlick,2014:“方法链接、流畅接口和完成问题或者为什么你不能同时拥有蛋糕和吃掉它”。 流畅界面是/曾经是一个伟大的风潮,适用于某些域(例如UI),但对于其他情况,无论是控制流异常还是需要倒回/撤销先前的调用都将是一个问题。 - smci
显示剩余3条评论
2个回答

8

这样做是可行的,我不确定是否有更好的方法,因为这是我的第一次尝试。祝你好运!

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

通过实现__call__方法,我们可以告诉代码我们已经到达调用链的末尾。当然,这会添加尾随的()调用,并要求您将指针更改为实际的消息传递方法和默认发送者变量,但我认为这将是在不知道链结束时实现目标的最简单方法。

嗨,懒惰的脚本编写者。我投了你的答案一票,因为它是一个非常简单而优美的解决方案,可以解决我的初始问题。然而,正如敏捷需求所要求的那样,你指出了一个在我的原始问题中缺失的事实:to()方法是必须的,而from()方法是可选的。由于你的想法基于"sender"和"receiver"这两个属性的存在,我认为它不会起作用。 - samba2
我已更新我的代码以适应您更新的参数。 - TheLazyScripter

4
返回 self 有一个缺点,就是如果修改其中一个变量,可能会影响到另一个变量。以下是一个示例,摘自 @TheLazyScripter 的代码并进行了修改。
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')()

变量a和b指向同一个实例。修改其中一个的值将影响另一个。请参见输出的第二行和第三行。

输出结果

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 且修改 a 的内容会影响到 b 的值。

如何解决这个副作用?

为了消除副作用,不要返回self,而是返回一个新的实例。

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')()
_clone 方法每次都会创建实例的一个新副本。注意:当其中一个值是列表或字典时,需要调用深拷贝。这里是字符串,因此不需要。但是思路是相同的,在返回之前复制每个属性。
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.

输出的第二行和第三行清楚地显示了新代码中没有任何副作用。

我写了一篇关于Python流畅接口(Fluent Interface)的博客文章。


我同意这个策略。另一个被认为是“不符合Python风格”的。 - TastyWheat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接