Предположим, что у нас есть очень нам нужный модуль, документация к которому представляет собой 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([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/
Добавить комментарий