Bound Inner Classes For Pythonpermalink categories: programming originally posted: 2013-12-24 09:39:02
(This blog entry is my contribution to the 2013 Python Advent Calendar. I'm the entry for December 25th,; however, due to the time zone difference between here and Japan, I'm posting it during what is to me the morning of the 24th. Merry Christmas!)
In Python, something magic happens when you put a function inside a class. If you access that function through an instance of the class, you don't simply get the function back. Instead you get a new object we call a "method". Conceptually, the function is "bound" to the instance; practically, when you call a method, it calls the function but automatically passes in the instance as the first positional argument, pushing the other positional arguments forward by one.
But this magic is only true for functions. Consider classes. In Python, if you define a class C inside another class D, we call C an "inner class" (or perhaps a "nested class"). If you access C through an instance of D, nothing magic happens, you just get C back, and if you call it the arguments to C.__init__ are unchanged.
Inner classes are rarely used in Python. Why? Because there are no practical advantages to doing so—but Python's scoping rules result in some minor inconveniences. (See below.)
I often use inner classes in Python anyway, because I have classes that conceptually should live inside some other class. And, most of the time, these inner classes conceptually want to be "bound" to the outer class instance. But I have to do it manually, like this:
class D: class C: def __init__(self, outer): self.outer = outer d = D() c = d.C(d)
Wouldn't it be nice if inner classes got "bound" the way functions did? Wouldn't it be nice if outer was passed in to the inner class's __init__ automatically?
But is this even possible in Python? Some years back I asked that question, on Stack Overflow:
I was amazed at the replies. People not only said that it was impossible, but that it was pointless. And then Alex Martelli showed it was possible—and his solution blew my mind! His solution uses a class decorator, and makes your class a "descriptor", which is the same mechanism functions use to become methods. Brilliant!
With Alex's permission, I posted this as a "recipe" at the Python Cookbook:
I've since fine-tuned the approach many times. At this point it works really well! And, yes, the recipe works unchanged in both Python 2 and 3.
I love bound inner classes and I use them whenever I can. When I tell people about bound inner classes, the most common feedback I get is "What's your use case?" My reply: "What's your use case for method calls?" I think it's the same question. And I suggest to you that bound inner classes are just as useful as methods. If not more so!
Of course, classes are more complicated than functions. So it makes sense that bound inner classes are more complicated than methods. For example, inheritance and more than two levels of nesting can make things a little crazy. I worked hard to make bound inner classes sensible and predictable in these situations—but you have to understand a little how they work, and obey some simple rules. All of this is documented in the "recipe" above.
I hope you start using bound inner classes in your own code!
In case you're wondering why inner classes can be inconvenient, consider the following:
class D: value = 5 class C: value2 = value * 5
This code doesn't work; it raises a NameError on value. Code in C can't see value—it can't see any members of D. All it can see are the members of C, globals, and builtins. (Unfortunately the nonlocal keyword doesn't help you here; that only helps with nested functions, and class bodies don't behave like nested functions.)
If you change it to this:
class D: value = 5 class C: value2 = D.value * 5
Sadly that doesn't work either. D doesn't exist yet—technically speaking, it "hasn't been bound yet". This code raises a NameError on D.
The only thing that works is to do the lookup at runtime:
class D: value = 5 class C: def __init__(self): self.value2 = D.value * 5
In other words, nested classes can't access any members of their outer classes at compile-time.
p.s. To my Japanese readers today on Christmas: I want to tell you that Americans don't actually eat KFC on Christmas day! This is a marketing gimmick created by KFC in Japan. Americans do usually have a big family dinner on Christmas, but there's no traditional main course.