Organizing Fluent Interfaces in Python

    Inspired by a recent post about fluid interfaces in PHP , I immediately wondered how to implement this in python easier and more beautiful (python is always simpler and more beautiful). I offer several ways in the order of receipt of thoughts.





    The first way - in the forehead


    To build a chain of operators, we need the function to return an instance of the class. This can be manually set.
    def add(self,x):
        self.val += x
        return self
    


    Obviously, this approach works perfectly, but we are looking for a little more.

    The second way - decorators


    This is the first idea that came to mind. Define a decorator for methods in the class. We are very happy that the instance is passed as the first argument.

    def chained(fn):
        def new(*args,**kwargs):
            fn(*args,**kwargs)
            return args[0]
        return new
    class UsefulClass1():
        def __init__(self,val): self.val = val
        @chained
        def add(self,val): self.val += val
        @chained
        def mul(self,val): self.val *= val
    


    We simply mark with the decorator the functions that need to be used in the chain. The return value is ignored and a class instance is passed instead.

    >>> print UsefulClass1(10).add(5).mul(10).add(1).val
    151
    


    The method is clearly more readable - you can immediately see what functions you can use in the chain. However, usually the architectural approach extends to most class methods that are not interesting to mark one at a time.

    The third way - automatic


    We can check the return value when calling the function. In the absence of one, we transfer the object itself. We will do this through __getattribute__, intercepting any access to the methods and fields of the class. To begin with, we simply define a class with similar behavior, we will inherit all working classes from it.

    from types import MethodType
    class Chain(object):
        def __getattribute__(self,item):
            fn = object.__getattribute__(self,item)
            if fn and type(fn)==MethodType:
                def chained(*args,**kwargs):
                    ans = fn(*args,**kwargs)
                    return ans if ans!=None else self
                return chained
            return fn
    class UsefulClass2(Chain):
        val = 1
        def add(self,val): self.val += val
        def mul(self,val): self.val *= val
        def third(self): return 386
    


    If the method returns a value, it is passed. If not, then the class instance itself goes instead.

    >>> print UsefulClass2().add(15).mul(16).add(-5).val
    251
    >>> print UsefulClass2().third()
    386
    


    Now we do not need to modify the working class in any way except specifying the chaining class as one of the parent ones. The obvious drawback is that we cannot use __getattribute__ for our own purposes.

    The fourth method - Im So Meta ...


    We can use the metaclass to organize the necessary wrapper of the working class. When initializing the latter, we will wrap __getattribute__ on the fly (and its absence does not bother us either).

    from types import MethodType
    class MetaChain(type):
        def __new__(cls,name,bases,dict):
            old = dict.get('__getattribute__',object.__getattribute__)
            def new_getattribute(inst,val):
                attr = old(inst,val)
                if attr==None: return inst
                if attr and type(attr)==MethodType:
                    def new(*args,**kwargs):
                        ans = attr(*args,**kwargs)
                        return ans if ans!=None else inst
                    return new
                return attr
            dict['__getattribute__'] = new_getattribute
            return type.__new__(cls,name,bases,dict)
    class UsefulClass3():
        __metaclass__ = MetaChain
        def __getattribute__(self,item):
            if item=="dp": return 493
            return object.__getattribute__(self,item)
        val = 1
        def add(self,val): self.val += val
        def mul(self,val): self.val *= val
    


    It’s practically no different from the previous version - we only control the creation of __getattribute__ using a metaclass. To wrap the working class, just specify __metaclass__.

    >>> print UsefulClass3().dp
    493
    >>> print UsefulClass3().add(4).mul(5).add(1).mul(25).add(-1).val
    649
    


    As you can see, the __getattribute__ originally available in the working class works. When inheriting from the working class, the behavior is preserved - __getattribute__ is also inherited. If the native __getattribute__ does not return anything (even an AttributeError), then we also return the object itself.

    Instead of a conclusion


    Although the ubiquitous use of fluid interfaces is doubtful, there are still instances when such structures will be appropriate. Say, image processing or any entities over which many operations are sequentially performed.

    PS The last option, in my opinion, does not contain obvious shortcomings. If anyone can offer the best option - I will be glad to hear, as well as indications of my shortcomings.

    PPS At the request of workers, links to the aforementioned article and description on Wikipedia

    Update

    The fifth method - roast and hearth


    Comrade davinchi rightly pointed out that wrap on every call is at least strange. Plus, we run a check every time we access the fields of the object.
    Now we will process all the methods at once, but we will check the modification and creation of the methods in order to wrap them.
    class NewMetaChain(type):
        def __new__(cls,name,bases,dict):
            old = dict.get('__setattr__',object.__setattr__)
            def wrap(fn,inst=None):
                def new(*args,**kwargs):
                    ans = fn(*args,**kwargs)
                    return ans if ans!=None else inst or args[0]
                return new
            special = dir(cls)
            for item, fn in dict.items():
                if item not in special and isinstance(fn,FunctionType):
                    dict[item] = wrap(fn)
            def new_setattr(inst,item,val):
                if isinstance(val,FunctionType):
                    val = wrap(val,inst)
                return old(inst,item,val)
            dict['__setattr__'] = new_setattr
            return type.__new__(cls,name,bases,dict)
    class UsefulClass4():
        __metaclass__ = NewMetaChain
        def __setattr__(self,item,val):
            if val == 172: val = "giza"
            object.__setattr__(self, item, val)
        val = 1
        def add(self,val): self.val += val
        def mul(self,val): self.val *= val
        def nul(self): pass
    

    In addition to the fact that now we don’t wrap methods with every call (which gave a ~ 30% increase in speed), we also carry out the necessary checks not on every reading of the object’s fields, but on each record (which happens less often). If there is no entry, it works as fast as the decorator method.

    Also popular now: