A blog by Bryn Keller about simplicity - in software design, and in life
Class properties are a feature that people coming to Python from other object-oriented languages expect, and expect to be easy. Unfortunately, it’s not. In many cases, you don’t actually want class properties in Python - after all, you can have first class module-level functions as well, you might very well be happier with one of those.
I sometimes see people claim that you can’t do class properties at all in Python, and that’s not right either. It can be done, and it’s not too bad. Read on!
I’m going to assume here that you already know what class (sometimes called “static”) properties are in languages like Java, and that you’re somewhat familiar with Python metaclasses.
To make this feature work, we have to use a metaclass. In this example, we’ll suppose that we want to be able to access a list of all the instances of our class, as well as reference to the most recently created instance. It’s artificial, but it gives us a reason to have both read-only and read-write properties. We define a metaclass, which is again a class that extends
class Extent(type): @property def extent(self): ext = getattr(self, '_extent', None) if ext is None: self._extent =  ext = self._extent return ext @property def last_instance(self): return getattr(self, '_last_instance', None) @last_instance.setter def last_instance(self, value): self._last_instance = value
Please note that if you want to do something like this for real, you may well need to protect access to these shared class properties with synchronization tools like RLock and friends to prevent different threads from overwriting each others’ work willy-nilly.
Next we create a class that uses that metaclass. The syntax is different in Python 2.7, so you may need to adjust if you’re working in an older version.
class Thing(object, metaclass=Extent): def __init__(self): self.__class__.extent.append(self) self.__class__.last_instance = self
Another note for real code: these references (the
extent and the
last_instance) will keep your object from being garbage collected, so if you actually want to keep extents for your classes, you should do so using something like weakref.
Now we can try out our new class:
>>> t1 = Thing() >>> t2 = Thing() >>> Thing.extent [<__main__.Thing object at 0x101c5d080>, <__main__.Thing object at 0x101c5d2b0>] >>> Thing.last_instance <__main__.Thing object at 0x101c5d2b0> >>>
Great, we have what we wanted! There are a couple of things to remember, though:
Let’s see an example that demonstrates both. Suppose we add a new subclass of
>>> class SuperThing(Thing): ... @property ... def extent(self): ... return self.__class__.extent ... >>> s = SuperThing()
See how we created a normal
extent property that just reads from the class property? So we can now do this:
>>> s.extent [<__main__.Thing object at 0x101c5d080>, <__main__.Thing object at 0x101c5d2b0>, <__main__.SuperThing object at 0x101c5d2e8>]
Whereas if we were to try that with one of the original
Things, it wouldn’t work:
>>> t1.extent Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Thing' object has no attribute 'extent'
We can of course still access either one via classes:
>>> t1.__class__.extent [<__main__.Thing object at 0x101c5d080>, <__main__.Thing object at 0x101c5d2b0>, <__main__.SuperThing object at 0x101c5d2e8>] >>> s.__class__.extent [<__main__.Thing object at 0x101c5d080>, <__main__.Thing object at 0x101c5d2b0>, <__main__.SuperThing object at 0x101c5d2e8>] >>>
Also note that the extent for each of these classes is the same, which shows that class properties are inherited.
Did you spot the bug in
Thing? It only manifests when we have subclasses like
SuperThing. We inherited the
Thing, which adds each new instance to the extent, and sets
last_instance. In this case,
self.__class__.extent was already initialized, on
Thing, and so we added our
SuperThing to the existing list. For
last_instance, however, we assigned directly, rather than first reading and appending, as we did with the list property, and so
SuperThing.last_instance will be our
Thing.last_instance will be our
t2. Tread carefully, it’s easy to make a mistake with this kind of thing!
Hopefully this has been a (relatively) simple example of how to build your own class properties, with or without setters.