Три рецепта питона

imageВ качестве продолжения прошлогодней статьи из серии «когда не надо, но хочется попробовать» хочу рассмотреть пример использования property(), модуля traceback и декораторов.

Предположим, что у нас есть очень нам нужный модуль, документация к которому представляет собой C++ исходники python bindings самого модуля и C++ исходники оригинального пакета. Ну и, конечно, dir(obj) с help(obj.method) немного упрощают жизнь. Но хочется большего: вменяемого если не автокомплита, то хотя бы py-модуля с перечнем методов каждого класса (имеющих описание, список и типы параметров и результата; a la pydoc). А вот бы еще получить словарь со всеми именами и значениями…

В качестве варианта реализации можно было бы просто оформить серию прокси-классов

from ourextmod import extClass class Class(object):     @accepts(extClass)     def __init__(self, extobj):         self._obj =  extobj      @returns(str)     def version(self):         return self._obj.version      @returns(str)     def name(self):         return self._obj.name      @accepts(str)     @name.setter     def name(self, value):         self._obj.name = value      @returns(dict)     def smth(self):         return self._obj.strange_original_method_name_for_smth_action()      @returns(str)     def full_name(self):         return self._obj.full_name()      @accepts(str)     @full_name.setter     def full_name(self, value):         self._obj.set_full_name(value)      def dump(self):         return {             'version'   : self.version,             'name'      : self.name,             'smth'      : self.smth,             'full_name' : self.full_name,         }         # можно через getattr; от дублирования кода это не избавляет 

В общем, на пятом десятке таких методов даже самый спокойный разработчик может начать нервничать…

Применяем property()

В качестве альтернативы парсинга сишных исходников для генерации классов попробуем создавать такие методы на лету через

property

Функция создает property-атрибут в классе, принимая на вход реализацию getter, setter, deleter и pydoc-описание (его опускаем для простоты):
property([fget[, fset[, fdel[, doc]]]])

Для имеющих getter и setter получаем:

    def prop(obj, name):         return property(lambda self: getattr(getattr(self, obj), name),                         lambda self, value: setattr(getattr(self, obj), name, value),                         None         ) 

С такой функцией работу с name можно заменить на:

class Class(object):     name = prop('_obj', 'name') 

Для read-only полей нужно задавать только getter:

    def prop_ro(obj, name):         return property(lambda self: getattr(getattr(self, obj), name),                         None,                         None         ) 

smth и full_name являются методами и для их описания нужно лишь добавить вызов в lambda-функции. Также full_name.setter отличается по имени метода, учтем это:

    def prop_call_ro(obj, name):         return property(lambda self: getattr(getattr(self, obj), name)(),                         None,                         None         )      def prop_call(obj, name, setter_name=None):         setter_name = setter_name if setter_name else name         return property(lambda self: getattr(getattr(self, obj), name)(),                         lambda self, value: getattr(getattr(self, obj), setter_name)(value),                         None         ) 

Завернув все наши поля в такие обертки, получаем:

class Class(object):     def __init__(self, extobj):         self._obj =  extobj      version = prop_ro('_obj', 'version')      name = prop('_obj', 'name')      smth = prop_call_ro('_obj', 'strange_original_method_name_for_smth_action')      full_name = prop_call('_obj', 'full_name', 'set_full_name') 

По сути мы лишились автокомплита (если он был до этого), но объем и восприятие класса значительно улучшились.

Автоматизируем dump()

Теперь уже хочется упростить dump(), чтобы не приходилось указывать список полей.
Если нужны все публичные поля, то можно было бы обойтись и [f for f in dir(self) if not f.startswith(‘_’)], но это не интересно 🙂

Хочется чтобы все поля, созданные через = prop*(…) автоматически отмечались как учитываемые в dump().

Создадим общий родительский класс, который будет делать за нас всю черновую работу:

class Dumpable(object):     @staticmethod     def prop(obj, name):         return property(lambda self: getattr(getattr(self, obj), name),                         lambda self, value: setattr(getattr(self, obj), name, value),                         None         )     ... 

Наш оригинальный класс немного поменяем:

class Class(Dumpable):     name = Dumpable.prop('_obj', 'name') 

Можно компактнее, но, имхо, так выходит понятнее что и откуда растет.
В итоге все prop*-поля попадают внутрь Dumpable. Так давайте же сохраним имена этих полей!

class Dumpable(object):     _props = {}      @staticmethod     def prop(obj, name):         Dumpable._add_me()         return ...     ...      @classmethod     def _add_me(cls):         prop_def = traceback.extract_stack()[-3]         cls_name = prop_def[2]         prop_name = prop_def[3].split('=')[0].strip()         cls.add_prop(cls_name, prop_name)         return      @classmethod     def add_prop(cls, cls_name, prop_name):         if not cls_name in cls._props:             cls._props[cls_name] = set()         cls._props[cls_name].add(prop_name)         return 

Dumpable.add_prop() добавляет во внутренний словарь имен классов со множествами имен полей указанную пару строк «имя класса» и «имя поля».
Dumpable._add_me(), априори зная, что вызывается только напрямую из prop*-методов самого Dumpable, выбирает из стека вызовов строчку с описанием поля (что-то вида «name = Dumpable.prop(‘_obj’, ‘name’)»), из которой получает уже имя поля «name». Заодно из стека вытягивается имя класса, в котором выполняется объявление поля.

Важно для понимания, что стек хранится в порядке от начального скрипта до текущей строки. В таком случае stack[-1] выдаст текущее положение, stack[-2] – точку вызова текущей функции и т.п.

В итоге, в словаре Dumpable._props у нас содержатся все имена классов и методов, описанных через prop*.

Дело остается за малым, реализовать dump():

class Dumpable(object):     ...      @classmethod     def _dump(cls, cls_name):         return cls._props.get(cls_name, set())      def dump(self, req=False):         props = Dumpable._dump(self.__class__.__name__)         results = {}         for key in props:             value = getattr(self, key)             if isinstance(value, Dumpable):                 value = value.dump() if req else value.__class__.__name__             results[key] = value         return results 

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

А благодаря
props = Dumpable._dump(self.__class__.__name__)
мы получаем имена полей именно для того класса, у которого вызывается метод dump().
Остается дело техники: перебрать все имена и получить значения. Если значением является другая инстанция Dumpable, то мы опционально либо делаем рекурсивный вызов dump, либо возвращаем название класса-потомка Dumpable (вот так захотелось).
В итоге реализацию dump() в Class можно вообще выкинуть.

Применяем декораторы

Все хорошо, пока нам не понадобится добавить произвольный метод в список dump-полей.

    class Class(Dumpable):         def foo(self):             return 42 

Не зря ранее были разделены реализации _add_me() и add_prop() в Dumpable. add_prop() нам теперь пригодится, нужно лишь вызвать его с указанием класа и имени метода. Но не руками же это делать. Тут поможет декоратор:

    class Dumpable(object):         @staticmethod         def decor(f):             return <магия>     ...     class Class(Dumpable):         @Dumpable.decor         def foo(self):             return 42 

Происходит магия и dump() начинает выдавать еще и foo.

Как начинающим волшебникам страны Оз нам осталось придумать эту магию.

    class Dumpable(object):     @staticmethod     def decor(f):         prop_name = f.__name__ # имя функции, на которую навешан наш декоратор          cls_name = traceback.extract_stack()[-2][2] # опять лезем в стек и достаем           Dumpable.add_prop(cls_name, prop_name)          def _(*args, **kwargs):             return f(*args, **kwargs)         return _ 

Однако, все работает до тех пор, пока наш декоратор указывается последним в списке.

        # так работает         @accepts(int, int)         @Dumpable.decor         def sum(self, a, b):             return a+b          # а так уже не будет         @accepts(int, int)         @Dumpable.decor         @returns(int)         def sub(self, a, b):             return a-b 

Дело в том, что во втором случае, приходящая в декоратор переменная f, уже не метод, а декоратор, объявленный после нашего (или целая их пачка, навернутых один над другим). И f.__name__ выдает совсем не то, что нам бы хотелось.
Но это все не помеха, достаточно раскрутить цепочку дектораторов до исходного метода:

    @staticmethod     def decor(f):         while f.func_closure is not None:             f = f.func_closure[0].cell_contents         if hasattr(f, 'func_name'):             prop_name = f.func_name         else:             raise RuntimeError('Impossible to calculate property name')          cls_name = traceback.extract_stack()[-2][2]          Dumpable.add_prop(cls_name, prop_name)          def _(*args, **kwargs):             return f(*args, **kwargs)         return _ 

Заданное значение для func_closure указывает на наличие замыкания поверх функции, до которой можно достучаться через f.func_closure[0].cell_contents. Вот так и раскручиваемся. В итоге получаем или нужное нам имя метода, либо оказываемся в глупом положении: например, при указании нашего декоратора на методе, которому ниже указан @property.

Теперь можно в чистой совестью навешивать декораторы как вздумается 🙂

    @property     @accepts(str)     @Dumpable.decor     @returns(dict)     def spam(self, cnt):         ... 

Автор не агитирует использовать решение целиком 🙂
Это лишь подборка нескольких частных случаев в одной решенной проблеме.

ссылка на оригинал статьи http://habrahabr.ru/post/174843/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *