Есть ли лучший способ обнаружить изменение атрибутов, чем использование декораторов?

Я проводил рефакторинг текстового класса, предназначенного для вывода текста на экран. Недавно я узнал о декораторе @property и подумал, что его использование в этом классе поможет.
Вот урезанная версия кода, который я пишу:

class Text:
    def __init__(self, text, antialias):
        self.text = text
        self.antialias = antialias
        self._render_text()

    @property
    def text(self):
        return self._text

    @text.setter
    def text(self, value):
        self._text = value
        self._requires_render = True

    @property
    def antialias(self):
        return self._antialias

    @antialias.setter
    def antialias(self, value):
        self._antialias = value
        self._requires_render = True

    def _render_text(self):
        self._required_render = False

    def blit(self):
        if self._requires_render:
            self._render_text()
        # blit text to screen

Я заметил, что моей основной мотивацией использования сеттеров и геттеров было изменение атрибута, который заказывал повторный рендеринг текста перед его выводом на экран. Это гарантировало правильное обновление атрибутов текстового объекта в цикле событий. Это также гарантировало, что если несколько свойств, все из которых требуют повторного рендеринга текста, были вызваны последовательно, будет запущен только один повторный рендеринг, поскольку проверка повторного рендеринга происходит только один раз в каждом игровом цикле.
Однако это также казалось чрезмерным, и мне было интересно, есть ли способ написать собственный объект @property, включающий строку self._requires_render. Хотя после некоторых поисков и не найдя ни у кого такой же проблемы, я считаю, что моя логика может быть ошибочной, и существует более простой способ определить, когда обновляются свойства объекта, и запустить некоторый код.

🤔 А знаете ли вы, что...
В Python можно легко работать с базами данных, такими как SQLite и MySQL.


51
1

Ответ:

Решено

Да, в идеале использовать те же механизмы, что и сам property, но собственность — это не идеал.

И property, и обычные методы в Python используют так называемый протокол descriptor — это означает, что атрибуты, связанные с классом, могут иметь методы __get__ и __set__, которые используются при доступе к значению с нотацией ..

По сути, все, что вам нужно, это метод __set__, который установит атрибут _requires_render и в противном случае обеспечит прозрачный доступ к атрибуту. Вы также можете воспользоваться методом __set_name__, который также связан с протоколом дескриптора и вызывается языком во время создания класса:

class RenderNeeded:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, instance, owner):
        if instance is None:
             return self
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        instance._requires_render = True
        instance.__dict__[self.name] = value



from functools import wraps


# and for methods instead of variables,
# this is the decorator approach:

def clear_rendering(func):
    @wraps(func)
    def wrapper(self, *args, **kw):
        result = func(self, *args, **kw)
        self._requires_render = False
        return result
    return wrapper
   
class Text:
    def __init__(...):
        ...
    text = RenderNeeded()
    antialias = RenderNeeded()
    
    @clear_rendering
    def blit(self, ...):
        # blit text code ...
        ...

Другой подход, кроме создания индивидуального дескриптора, — настроить метод __setattr__ непосредственно в вашем классе:

class Text:
    def __init__(self, text, antialias):
         ...
    def __setattr__(self, attr, value):
         if attr in {"text", "antialias", ...}:
               # this triggers a recursive call, but there is no problem. Just use `super().__setattr__` to avoid it, if preferred:
               self._requires_render = True
          return super().__setattr__(attr, value)