Pull to refresh

Организация текучих (fluent) интерфейсов в Python

Reading time 4 min
Views 5.5K
Вдохновлённый недавним постом про текучие интерфейсы на PHP, я сразу задумался как можно реализовать подобное на питоне проще и красивее (на питоне всегда всё проще и красивее). Предлагаю несколько способов в порядке поступления мыслей.





Способ первый — в лоб


Для построении цепочки операторов нам необходимо чтобы функция возвращала экземпляр класса. Это можно вручную задать.
def add(self,x):
    self.val += x
    return self


Очевидно что такой подход работает идеально, но мы ищем немножко другое.

Способ второй — декораторы


Это первая идея, пришедшая в голову. Определяем декоратор для методов в классе. Нам очень на руку что экземпляр передаётся первым аргументом.

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


Просто помечаем декоратором функции, которые нужно использовать в цепочке. Возвращаемое значение игнорируется и вместо него передаётся экземпляр класса.

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


Способ явно читабельнее — сразу видно какие функции можно использовать в цепочке. Однако, обычно архитектурный подход распространяется на большинство методов класса, которые помечать по одному не интересно.

Способ третий — автоматический


Мы можем при вызове функции проверять возвращаемое значение. При отсутствии оного мы передаём сам объект. Делать это будем через __getattribute__, перехватывающего любой доступ к методам и полям класса. Для начала просто определим класс с подобным поведением, все рабочие классы будем наследовать от него.

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


Если метод возвращает значение — оно передаётся. Если нет — то вместо него идёт сам экземпляр класса.

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


Теперь нам не надо никак модифицировать рабочий класс кроме указания класса-цепочки как одного из родительских. Очевидный недостаток — мы не можем использовать __getattribute__ в своих целях.

Способ четвёртый — Im So Meta...


Мы можем использовать метакласс для организации нужной обёртки рабочего класса. При инициализации последнего мы будем на лету обёртывать __getattribute__ (причём его отсутствие нам тоже не мешает).

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


От предыдущего варианта практически ничем не отличается — мы только контролируем создание __getattribute__ с помощью метакласса. Для обёртки рабочего класса достаточно указать __metaclass__.

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


Как видно, имеющийся изначально __getattribute__ в рабочем классе работает. При наследовании от рабочего класса поведение сохраняется — __getattribute__ тоже наследуется. Если родной __getattribute__ не возвращает ничего (даже AttributeError), то мы тоже возвращаем сам объект.

Вместо заключения


Хотя повсеместное применение текучих интерфейсов сомнительно, но всё же есть случаи когда подобные структуры будут уместны. Скажем, обработка изображений или любых сущностей, над которыми последовательно проводится множество операций.

PS Последний вариант, по моему мнению, не содержит очевидных недостатков. Если кто может предложить лучший вариант — буду рад выслушать, равно как и указания недостатки моего.

PPS По просьбам трудящихся ссылки на упомянутую статью и описание в википедии

Update

Способ пятый — жаркое и очаг


Тов. davinchi справедливо указал что обёртывать на каждом вызове по меньшей мере странно. Плюсом к этому, мы при каждом обращении к полям объекта прогоняем проверку.
Теперь мы обработаем все методы сразу, но будем проверять модификацию и создание методов для того чтобы и их обернуть.
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

Помимо того что мы теперь при каждом вызове не оборачиваем методы (что дало ~30% прироста в скорости), мы ещё проводим необходимые проверки не на каждом считывании полей объекта, а на каждой записи (что происходит реже). Если запись отсутствует — работает так же быстро как и способ с декораторами.
Tags:
Hubs:
+59
Comments 34
Comments Comments 34

Articles