Real-life example – lazily evaluated attributes
One example usage of descriptors may be to delay initialization of the class attribute to the moment when it is accessed from the instance. This may be useful if the initialization of such attributes depends on the global application context. The other case is when such initialization is simply expensive, but it is not known whether it will be used anyway when the class is imported. Such a descriptor could be implemented as follows:
class InitOnAccess: def __init__(self, klass, *args, **kwargs): self.klass = klass self.args = args self.kwargs = kwargs self._initialized = None def __get__(self, instance, owner): if self._initialized is None: print('initialized!') self._initialized = self.klass(*self.args,
**self.kwargs) else: print('cached!') return self._initialized
Here is an example usage:
>>> class MyClass: ... lazily_initialized = InitOnAccess(list, "argument") ... >>> m = MyClass() >>> m.lazily_initialized initialized! ['a', 'r', 'g', 'u', 'm', 'e', 'n', 't'] >>> m.lazily_initialized cached! ['a', 'r', 'g', 'u', 'm', 'e', 'n', 't']
The official OpenGL Python library available on PyPI under the PyOpenGL name uses a similar technique to implement a lazy_property object that is both a decorator and a data descriptor:
class lazy_property(object): def __init__(self, function): self.fget = function def __get__(self, obj, cls): value = self.fget(obj) setattr(obj, self.fget.__name__, value) return value
Such an implementation is similar to using the property decorator (described later), but the function that is wrapped with it is executed only once and then the class attribute is replaced with a value returned by that function property. That technique is often useful when there's a need to fulfil the following two requirements at the same time:
- An object instance needs to be stored as a class attribute that is shared between its instances (to save resources)
- This object cannot be initialized at the time of import because its creation process depends on some global application state/context
In the case of applications written using OpenGL, you can encounter this kind of situation very often. For example, the creation of shaders in OpenGL is expensive because it requires a compilation of code written in OpenGL Shading Language (GLSL). It is reasonable to create them only once, and, at the same time, include their definition in close proximity to classes that require them. On the other hand, shader compilations cannot be performed without OpenGL context initialization, so it is hard to define and compile them reliably in a global module namespace at the time of import.
The following example shows the possible usage of the modified version of PyOpenGL's lazy_property decorator (here, lazy_class_attribute) in some imaginary OpenGL-based application. The highlighted change to the original lazy_property decorator was required in order to allow the attribute to be shared between different class instances:
import OpenGL.GL as gl from OpenGL.GL import shaders class lazy_class_attribute(object): def __init__(self, function): self.fget = function def __get__(self, obj, cls):
value = self.fget(obj or cls)
# note: storing in class object not its instance
# no matter if its a class-level or
# instance-level access
setattr(cls, self.fget.__name__, value)
return value
class ObjectUsingShaderProgram(object): # trivial pass-through vertex shader implementation VERTEX_CODE = """ #version 330 core layout(location = 0) in vec4 vertexPosition; void main(){ gl_Position = vertexPosition; } """ # trivial fragment shader that results in everything # drawn with white color FRAGMENT_CODE = """ #version 330 core out lowp vec4 out_color; void main(){ out_color = vec4(1, 1, 1, 1); } """ @lazy_class_attribute def shader_program(self): print("compiling!") return shaders.compileProgram( shaders.compileShader( self.VERTEX_CODE, gl.GL_VERTEX_SHADER ), shaders.compileShader( self.FRAGMENT_CODE, gl.GL_FRAGMENT_SHADER ) )
Like every advanced Python syntax feature, this one should also be used with caution and documented well in code. For inexperienced developers, the altered class behavior might be very confusing and unexpected, because descriptors affect the very basic part of class behavior. Because of that, it is very important to make sure that all your team members are familiar with descriptors and understand this concept well if it plays an important role in your project's code base.