Fluent interface in python

Fluent Interface is an implementation of API which improves readability.

Example

Poem('The Road Not Taken').indent(4).suffix('Robert Frost').

Fluent Interface is similar to method chaining. I was wondering how to implement this in Python.Returning self during method call seemed good idea .

class Poem(object):
    def __init__(self, content):
        self.content = content

    def indent(self, spaces=4):
        self.content = " " * spaces + self.content
        return self

    def suffix(self, content):
        self.content += " - {}".format(content)
        return self

    def __str__(self):
        return self.content

>>>print Poem('Road Not Taken').indent(4).suffix('Rober Frost').content
    Road Not Taken - Rober Frost

Everything seems to be ok here.

Side effects

Above approach has side effect. We are mutating the same object during every method call.Consider the following code

>>>p = Poem('Road Not Taken')
>>>q = p.indent(4)
>>>r = p.indent(2)
>>>print str(q) == str(r)
True
>>>print id(q), id(r)
4459640464 4459640464

Clearly this isn’t expected. q and r is pointing to same instance.Ideally we should create a new instance during every method call and return the object.

New object

Decorator is a function which takes a function as argument and returns a function (most cases).

from functools import wraps

def newobj(method):
    @wraps(method)
    # Well, newobj can be decorated with function, but we will cover the case
    # where it decorated with method
    def inner(self, *args, **kwargs):
        obj = self.__class__.__new__(self.__class__)
        obj.__dict__ = self.__dict__.copy()
        method(obj, *args, **kwargs)
        return obj
    return inner


class NPoem(object):
    def __init__(self, content):
        self.content = content

    @newobj
    def indent(self, spaces=4):
        self.content = " " * spaces + self.content

    @newobj
    def suffix(self, content):
        self.content += " - {}".format(content)

    def __str__(self):
        return self.content

>>>p = NPoem("foo")
>>>q = p.indent(4)
>>>r = p.indent(2)
>>>print str(q) == str(r)
False
>>>print(q)
    foo
>>>print(r)
  foo
>>>print id(q), id(r)
 4459642640 4459639248

In the above approach newobj decorator creates a new instance, copies all the atrributes,calls the method with arguments and returns new instance. Rather than using decoratorprivate function can do the same.

Fluent Interface is used heavily in SQLAlchemy and Django. Djangouses _clone method to create new QuerySet andSQLAlchemy usesdecorator based approach to create new Query instance.

Thanks Mike Bayer and Daniel Roy Greenfeldhelping me understand this.

Do you know any other way to implement this ? Feel free to comment.

See also

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Powered by Buttondown.