I have been refactoring a text class meant for blitting text to the screen.
I recently learned about the @property
decorator and thought using it in this class would help.
Here is a stripped down version of the code I am writing:
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
I noticed that my main motivation for using the setters and getters was to change an attribute that ordered the re-render of the text before blitting to the screen. This ensured that changing attributes of the text object correctly updated in the event loop. It also ensured that if multiple properties that all required the text to be re-rendered were called in sucession, only one re-render would be triggered as the re-render check happens only once every gameloop.
However it also seemed excessive and I was wondering if there was a way to write a custom @property
object that included the self._requires_render
line. Though after some searching and not finding anyone having the same problem, I am considering that my logic may be flawed and there is an easier way of detecting when an object's properties update, and running some code.
Yes - the ideal thing there is to use the same mechanisms used by property
itself - but property is not the ideal.
Both property
and normal methods in Python use whatis called the descriptor
protocol - which means that the attributes associated in the class can have a __get__
and __set__
method, which are used when accessing the value with the .
notation.
Basically, all you need is a __set__
method that will set the _requires_render
attribute, and otherwise allow transparent access to the attribute. You can take advantage of the __set_name__
method which is asscoaited to the descriptor protocol as well, and is called at class creation time by the language:
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 ...
...
Another approach than creating a customized descriptor is to customize the __setattr__
method, directly in your class:
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)