📜 ⬆️ ⬇️

Fluent Interface Organization in Python

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




')

Method one - 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.

Method two - decorators


This is the first idea that came to mind. Define a decorator for methods in the class. We are very good that the instance is passed by 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 


Simply mark the functions to be used in the chain with the decorator. The return value is ignored and an instance of the class is passed instead.

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


The method is clearly more readable - you can immediately see which functions can be used in the chain. However, usually the architectural approach extends to most class methods, which are not interesting to mark one by one.

Method three - automatic


We can check the return value when calling a function. In the absence thereof, we transfer the object itself. We will do this through __getattribute__, which intercepts any access to the methods and fields of the class. To begin with, we simply define a class with similar behavior; all working classes will inherit 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 an instance of the class goes instead.

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


Now we don’t need to modify the working class in any way except for specifying the chain class as one of the parent classes. The obvious drawback is that we cannot use __getattribute__ for our own purposes.

Method Four - Im So Me 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 doesn’t 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 practically does not differ from the previous version - we only control the creation of __getattribute__ with the help of the metaclass. For a working class wrapper, just enter __metaclass__.

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


As you can see, the existing __getattribute__ 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 AttributeError), then we also return the object itself.

Instead of conclusion


Although the widespread use of fluid interfaces is doubtful, there are still cases when such structures will be appropriate. Say, for example, image processing or any entities over which a number of operations are performed.

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

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

Update

Method Five - hot and hearth


Tov. davinchi rightly pointed out that wrapping on every call is at least strange. Plus to this, we with each reference to the fields of the object run the check.
Now we will process all methods at once, but we will check the modification and creation of 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 

Besides the fact that we do not wrap methods every time we call (which yielded ~ 30% gain in speed), we still do the necessary checks not on every read of the object fields, but on each record (which happens less often). If there is no record, it works as fast as the decorator method.

Source: https://habr.com/ru/post/132317/


All Articles