В качестве продолжения прошлогодней статьи из серии «когда не надо, но хочется попробовать» хочу рассмотреть пример использования 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([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):
c = f
while c.func_closure is not None:
c = c.func_closure[0].cell_contents
if hasattr(c, 'func_name'):
prop_name = c.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):
...
Автор не агитирует использовать решение целиком :)
Это лишь подборка нескольких частных случаев в одной решенной проблеме.
Автор: AterCattus