These snippets are just a short reminder of how class variables work in Python. I understand this topic a bit too well, I think; I always remember the gotchas and can’t remember which gotcha belongs to which important detail. I generally come up with the right answer then convince myself I’m wrong until I write a bit of code and experiment. Hopefully this snippet will shortcut that process.
Consider the following class hierarchy:
from itertools import count
class Foo(object):
counter = count()
def __init__(self):
self.id = self.counter.next()
def __str__(self):
return "{} {}".format(self.__class__.__name__, self.id)
class Bar(Foo):
pass
class Baz(Bar):
pass
Every instance of Foo
will be assigned a unique, automatically incrementing id using the count
iterator for itertools
. The thing to remember is that Bar
and Baz
are also instances of Foo
:
>>> isinstance(Baz(), Foo)
True
Keep that in mind given the following code:
>>> import random
>>> things = (Foo, Bar, Baz)
>>> for _ in xrange(10):
... print random.choice(things)()
What is the expected result? If you said something like as follows:
Bar 0
Baz 1
Foo 2
Baz 3
Foo 4
Bar 5
Bar 6
Bar 7
Foo 8
Bar 9
Then you’re on the right track.
The problem is that the code above is typically not what is meant by programmers. And while I typically come to the conclusion that what I’m actually expressing by the above code is counting instances of Foo
, what I actually want to do is count instances of each class (how many of each Foo
, Bar
, and Baz
).
Then I realize … oh crap, I’ve strayed into metaprogramming land. And that’s why I need the reminder of this post. I definitely get that I need a metaclass to make subclass counters work as expected, but I never remember exactly how to do it. So here’s how.
class Countable(type):
def __new__(cls, name, bases, attrs):
attrs['counter'] = count()
return super(Countable, cls).__new__(cls, name, bases, attrs)
class Foo(object):
__metaclass__ = Countable
Basically, what we’ve done here is is told the Foo class that it should be constructed using Countable
instead of type
. When the class is created, therefore it is given the class attribute counter
. Now the output is as follows:
Foo 0
Bar 0
Foo 1
Baz 0
Bar 1
Foo 2
Foo 3
Foo 4
Baz 1
Foo 5
This post isn’t about a long discussion on the metaclass in Python or how type
is a subclass of type
, but simply serves as a reminder for the very rare occasion that I have to rock something other than type
. For more information on the subject, a very nice write-up, A Primer on Python Metaclasses by @jakevdp is the way to go.