-
25 April 2010, 23:06
Python Descriptors
To know Python, you must know descriptors. They are used in Python to implement new-style classes. Descriptors can simplify your code and prevent you from performing expensive functions. I had a short tutorial up before, but I had to remove it since it wasn't perfect. Thanks Paul!
Here, I'll be using a concept found in Django to illustrate descriptors. First, let's take a look at two classes.
class Shared(object): __NIL = object() def __init__(self, initializer_func): self._initializer = initializer_func self._cache = self.__NIL def __set__(self, instance, val): raise AttributeError, u'Read-only attribute' def __get__(self, instance, cls): if self._cache == self.__NIL: if callable(self._initializer): self._cache = self._initializer(instance) else: self._cache = self._initializer return self._cache class Unique(object): def __init__(self, name, initializer): self.initializer = initializer self.var = name def __set__(self, instance, val): raise AttributeError, u'Read-only attribute' def __get__(self, instance, cls): if not hasattr(instance, self.var): if callable(self.initializer): setattr(instance, self.var, self.initializer(instance)) else: setattr(instance, self.var, self.initializer) return getattr(instance, self.var)Well, there are two different implementations of descriptors. What makes them descriptors? Basically, two little functions:
__get__and__set__. How are these descriptor classes used? Simple:from random import random class DescriptorUser(object): def expensive_func(self): return random() #ok, this is just an example shared = Shared(expensive_func) unique = Unique('_u', expensive_func) a = DescriptorUser() b = DescriptorUser() c = DescriptorUser() d = DescriptorUser() print a.shared, b.shared, c.shared, d.shared print a.unique, b.unique, c.unique, d.uniqueHere we have a simple class that defines an expensive function—though this is not very expensive here for demonstration purposes.
sharedanduniqueare instances of their respective classes, but when they are accessed, they are accessed as though they were normal properties of the instance. This is where those special__get__and__set__methods come in to play. When the property is retrieved,__get__is called, supplying the instance and the class as arguments. When the property is being modified,__set__is called, supplying the instance and new value as arguments. Now, I have omitted checks on theinstanceargument. I recommend checking that the property is the property of an instance and throwing an Error if not.Let's take a closer look at the two
__get__methods. It is important to note that when a descriptor is made, it is shared by all instances of the class. TheSharedclass, in it's__get__method checks whether or not the cache is set. If not, it uses the supplied initializer function to set it (this is the expensive function that we have defined). Since the instance ofSharedis shared between all instances ofDescriptorUser,a.shared,b.shared, etc. will all return the same value.Uniqueis a little different. It checks a given attribute on the supplied instance. In this case, it checksinstancefor the_uattribute. If it has been set, use it. If not, set it using the supplied initializer function.I know descriptors can be a bit much at first, but keep playing around with them. Build upon the examples here by adding the check to see if the instance is actually an object. Then move into changing the
__set__function to actually set something. There's even one more descriptor method that I have left for the reader:__delete__. Here is the full source together, so you don't have to paste all different items separately:#!/usr/bin/python from random import random class Shared(object): __NIL = object() def __init__(self, initializer_func): self._initializer = initializer_func self._cache = self.__NIL def __set__(self, instance, val): raise AttributeError, u'Read-only attribute' def __get__(self, instance, cls): if self._cache == self.__NIL: if callable(self._initializer): self._cache = self._initializer(instance) else: self._cache = self._initializer return self._cache class Unique(object): def __init__(self, name, initializer): self.initializer = initializer self.var = name def __set__(self, instance, val): raise AttributeError, u'Read-only attribute' def __get__(self, instance, cls): if not hasattr(instance, self.var): if callable(self.initializer): setattr(instance, self.var, self.initializer(instance)) else: setattr(instance, self.var, self.initializer) return getattr(instance, self.var) class DescriptorUser(object): def expensive_func(self): return random() #ok, this is just an example shared = Shared(expensive_func) unique = Unique('_u', expensive_func) a = DescriptorUser() b = DescriptorUser() c = DescriptorUser() d = DescriptorUser() print a.shared, b.shared, c.shared, d.shared print a.unique, b.unique, c.unique, d.unique print vars(a)