Три рецепта python’а

в 10:43, , рубрики: decorator, property, python, traceback, метки: , , ,

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):
        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

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js