This recipe provides the class decorator BoundInnerClass
. The decorator makes inner classes symmetric with method calls. Functions declared inside classes become "methods", and when you call them through an object they automatically get a reference to "self". The BoundInnerClass
decorator makes this work for inner classes: an inner class decorated with BoundInnerClass
gets a reference to that same (now "outer") object passed in automatically to the inner class's __init__
.
The recipe works unchanged in Python 2.6 and 3.1, and is licensed using the Zlib license.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """
http://code.activestate.com/recipes/577070-bound-inner-classes/
Copyright (C) 2010-2011 by Alex Martelli and Larry Hastings
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
__all__ = ["BoundInnerClass", "UnboundInnerClass"]
import weakref
class _Worker(object):
def __init__(self, cls):
self.cls = cls
def __get__(self, outer, outer_class):
if not outer:
return self.cls
name = self.cls.__name__
# if we're already cached in outer, use that version
# (but don't use getattr, that would call __get__ recursively)
if name in outer.__dict__:
return outer.__dict__[name]
wrapper_bases = [self.cls]
# iterate over cls's bases and look in outer to see
# if any have bound inner classes in outer.
# if so, multiply inherit from the bound inner version(s).
multiply_inherit = False
for base in self.cls.__bases__:
# if we can't find a bound inner base,
# add the original unbound base instead.
# this is harmless but helps preserve the original MRO.
inherit_from = base
# if we inherit from a boundinnerclass from another outer class,
# we might have the same name as a legitimate base class.
# but if we look it up, we'll call __get__ recursively... forever.
# if the name is the same as our own name, there's no way it's a
# bound inner class we need to inherit from, so just skip it.
if base.__name__ != name:
bound_inner_base = getattr(outer, base.__name__, None)
if bound_inner_base:
bases = getattr(bound_inner_base, "__bases__", (None,))
# the unbound class is always the first base of
# the bound inner class.
if bases[0] == base:
inherit_from = bound_inner_base
multiply_inherit = True
wrapper_bases.append(inherit_from)
Wrapper = self._wrap(outer, wrapper_bases[0])
Wrapper.__name__ = name
# Assigning to __bases__ is startling, but it's the only way to get
# this code working simultaneously in both Python 2 and Python 3.
if multiply_inherit:
Wrapper.__bases__ = tuple(wrapper_bases)
# cache in outer, but only if we're replacing the class
# (if we're a bound inner class of A, and there's class B(A),
# and in class B we're some other kind of attribute,
# *don't* stomp on it!)
if getattr(outer.__class__, name, None) == self.cls:
setattr(outer, name, Wrapper)
return Wrapper
class BoundInnerClass(_Worker):
def _wrap(self, outer, base):
wrapper_self = self
assert outer
outer_weakref = weakref.ref(outer)
class Wrapper(base):
def __init__(self, *args, **kwargs):
wrapper_self.cls.__init__(self,
outer_weakref(), *args, **kwargs)
# give the bound inner class a nice repr
# (but only if it doesn't already have a custom repr)
if wrapper_self.cls.__repr__ is object.__repr__:
def __repr__(self):
return "".join([
"<",
self.__module__,
".",
self.__class__.__name__,
" object bound to ",
repr(outer_weakref()),
" at ",
hex(id(self)),
">"])
return Wrapper
class UnboundInnerClass(_Worker):
def _wrap(self, outer, base):
class Wrapper(base):
pass
return Wrapper
# The code in this "if" statement will only execute if you run the module
# directly; it won't run if you "import" this code into your own programs.
if __name__ == "__main__":
class Outer(object):
@BoundInnerClass
class Inner(object):
def __init__(self, outer):
self.outer = outer
@BoundInnerClass
class SubclassOfInner(Inner.cls):
def __init__(self, outer):
super(Outer.SubclassOfInner, self).__init__()
assert self.outer == outer
@BoundInnerClass
class SubsubclassOfInner(SubclassOfInner.cls):
def __init__(self, outer):
super(Outer.SubsubclassOfInner, self).__init__()
assert self.outer == outer
@BoundInnerClass
class Subclass2OfInner(Inner.cls):
def __init__(self, outer):
super(Outer.Subclass2OfInner, self).__init__()
assert self.outer == outer
class RandomUnboundInner(object):
def __init__(self):
super(Outer.RandomUnboundInner, self).__init__()
pass
@BoundInnerClass
class MultipleInheritanceTest(SubclassOfInner.cls,
RandomUnboundInner,
Subclass2OfInner.cls):
def __init__(self, outer):
super(Outer.MultipleInheritanceTest, self).__init__()
assert self.outer == outer
@UnboundInnerClass
class UnboundSubclassOfInner(Inner.cls):
pass
def tests():
assert outer.Inner == outer.Inner
assert isinstance(inner, outer.Inner)
assert isinstance(inner, Outer.Inner)
assert isinstance(subclass, Outer.SubclassOfInner)
assert isinstance(subclass, outer.SubclassOfInner)
assert isinstance(subclass, Outer.Inner)
assert isinstance(subclass, outer.Inner)
assert isinstance(subsubclass, Outer.SubsubclassOfInner)
assert isinstance(subsubclass, outer.SubsubclassOfInner)
assert isinstance(subsubclass, Outer.SubclassOfInner)
assert isinstance(subsubclass, outer.SubclassOfInner)
assert isinstance(subsubclass, Outer.Inner)
assert isinstance(subsubclass, outer.Inner)
import itertools
for order in itertools.permutations([1, 2, 3]):
outer = Outer()
# This strange "for" statement lets us test every possible order of
# initialization for the "inner" / "subclass" / "subsubclass" objects.
for which in order:
if which == 1: inner = outer.Inner()
elif which == 2: subclass = outer.SubclassOfInner()
elif which == 3: subsubclass = outer.SubsubclassOfInner()
tests()
multiple_inheritance_test = outer.MultipleInheritanceTest()
assert outer.MultipleInheritanceTest.mro() == [
# bound inner class, notice lowercase-o "outer"
outer.MultipleInheritanceTest,
# unbound inner class, notice uppercase-o "Outer"
Outer.MultipleInheritanceTest,
outer.SubclassOfInner, # bound
Outer.SubclassOfInner, # unbound
Outer.RandomUnboundInner, # etc.
outer.Subclass2OfInner,
Outer.Subclass2OfInner,
outer.Inner,
Outer.Inner,
object
]
unbound = outer.UnboundSubclassOfInner()
assert outer.UnboundSubclassOfInner.mro() == [
outer.UnboundSubclassOfInner,
Outer.UnboundSubclassOfInner,
outer.Inner,
Outer.Inner,
object
]
class InnerChild(outer.Inner):
pass
inner_child = InnerChild()
isinstance(inner_child, Outer.Inner)
isinstance(inner_child, InnerChild)
isinstance(inner_child, outer.Inner)
|
Overview
One minor complaint about Python is that inner classes don't have access to the outer object. Consider this Python code:
class Outer(object):
def method(self):
pass
class Inner(object):
def __init__(self):
pass
o = Outer()
o.method()
i = o.Inner()
When o.method
is called, Python automatically passes in the o
object as the first parameter (generally called "self"
). But there's no built-in way for the o.Inner
object being constructed to automatically get a reference to the o
Outer
object. If you need one, you must pass it in yourself like so:
class Outer(object):
def method(self):
pass
class Inner(object):
def __init__(self, outer):
self.outer = outer
o = Outer()
o.method()
i = o.Inner(o)
It seems redundant to have to pass in o
as an argument to the Inner
constructor. You don't have to pass in o
explicitly to method calls; why should you have to pass it in explicitly to inner classes?
I posted a question online to see if anyone could propose a way to automatically pass the outer instance into the inner class's __init__
. Alex Martelli responded with a brilliant solution: a class decorator, itself implemented as a class providing a descriptor, which internally subclasses the passed-in class and passes in a reference to the object to the class's __init__
. The result has a pleasing symmetry with normal callables and "bound method objects". After trading some edits back and forth, Alex and I are pleased to present our solution below, which I've called "bound inner classes".
Using Bound Inner Classes
Let's modify the above example to use the recipe:
from boundinnerclass import BoundInnerClass
class Outer(object):
def method(self):
pass
@BoundInnerClass
class Inner(object):
def __init__(self, outer):
self.outer = outer
o = Outer()
o.method()
i = o.Inner()
All we did:
- Import the recipe.
- Decorate the inner class with
BoundInnerClass
. - Stop passing in the outer object
o
to the constructor. It's now passed in automatically!
An Example Use Case
In case you're wondering "why would I ever care about this", consider the following only-slightly-contrived example.
Let's say you have a class that represents performing some task. The class supports the __call__
method, making it directly callable to do its work. Something like:
class Worker(object):
def __init__(self, ...):
...
def __call__(self):
# do work here
...
Now let's say you get more sophisticated one day, and want to wrap all that up in a thread. The most convenient way to create threads in Python is by subclassing the threading.Thread
class. However, the documentation for threading.Thread
tells you how to override the class's __init__
and run
methods, then states:
No other methods (except for the constructor) should be overridden in a subclass. In other words, only override the
__init__()
andrun()
methods of this class.
Because of this restriction, it's generally best not to subclass threading.Thread
in your own general-purpose classes. Instead, you should write a special class, whose whole purpose in life is to subclass threading.Thread
and override just those two methods.
You could easily write it like this:
class Worker(object):
...
class WorkerThread(threading.Thread):
def __init__(self, worker):
super(WorkerThread, self).__init__()
self.worker = worker
def run(self):
self.worker()
worker = Worker()
thread = WorkerThread(worker)
Clearly this works. But I suggest it can be improved. Conceptually, WorkerThread
really has no utility apart from servicing the Worker
class. So I suggest it should be an inner class:
class Worker(object):
...
class Thread(threading.Thread):
def __init__(self, worker):
super(WorkerThread, self).__init__()
self.worker = worker
def run(self):
self.worker()
But now your code for initialization looks a bit silly:
worker = Worker()
thread = worker.Thread(worker)
Here you are, calling an attribute of worker
, and yet you must also pass in worker
? Why doesn't it automatically know about worker
?
You can hide this redundancy by making a method of Worker
doing the redundant passing-in for you:
class Worker(object):
...
class Thread(threading.Thread):
...
def create_thread(self):
return self.Thread(self)
worker = Worker()
thread = worker.create_thread()
But this really creates more problems than it solves. You've got two callable things when one should suffice. This clutters up the namespace of the class, not to mention cluttering your mind. If someone wants to create one of these thread objects, they must use one interface, but if they want to do an isinstance()
test they must use the other.
Decorating the inner Thread
class with BoundInnerClass
solves this problem:
class Worker(object):
...
@BoundInnerClass
class Thread(threading.Thread):
def __init__(self, worker):
...
worker = Worker()
thread = worker.Thread()
Written this way, the Thread
constructor is now automatically called with the "outer" object, in this case the worker
object, without anyone having to explicitly--redundantly--pass it in. And everything works as you'd expect: you can tear off the worker.Thread
callable and call it as a standalone function, and you can run an isinstance()
test which will report success.
Inheritance
Bound inner classes get slightly complicated when mixed with inheritance. It's not all that difficult, you merely need to obey the following rules:
- A bound inner class can inherit normally from any unbound class.
- To subclass from a bound inner class while inside the same outer class, or when referencing the inner class from the outer class (as opposed to an instance of the outer class), you must actually subclass from
classname.cls
. (This is because inside the outer class, the "class" you see is actually an instance of aBoundInnerClass
object.) - All classes that inherit from a bound inner class must always call call the superclass's
__init__
. You don't need to pass in theouter
parameter; it'll be automatically passed in to the superclass's__init__
as before. - An inner class that inherits from a bound inner class, and which also wants to be bound to the outer object, should be decorated with
BoundInnerClass
. - An inner class that inherits from a bound inner class, but isn't interested in being bound to the outer object, should be decorated with this recipe's other decorator,
UnboundInnerClass
.
Restating the last two rules: every class that descends from any BoundInnerClass
should be decorated with either BoundInnerClass
or UnboundInnerClass
.
Here's a simple example of inheritance with bound inner classes:
from boundinnerclass import BoundInnerClass
class Outer(object):
@BoundInnerClass
class Inner(object):
def __init__(self, outer):
self.outer = outer
@UnboundInnerClass
class ChildOfInner(Inner.cls):
def __init__(self):
super(Outer.ChildOfInner, self).__init__()
o = Outer()
i = o.ChildOfInner()
We followed the rules:
Inner
inherits fromobject
; sinceobject
isn't a bound inner class, there are no special rules about inheritanceInner
needs to obey.ChildOfInner
inherits fromInner.cls
, notInner
.- Since
ChildOfInner
inherits from aBoundInnerClass
, it must be decorated with eitherBoundInnerClass
orUnboundInnerClass
. It doesn't want theouter
object passed in, so it's decorated withUnboundInnerClass
. ChildOfInner.__init__
calls the__init__
of itssuper()
.
Note that, because ChildOfInner
is decorated with UnboundInnerClass
, it doesn't take a outer
argument. Nor does it pass one in when it it calls super().__init__
. But when the constructor for Inner
is called, the correct outer
argument appears like magic. Thanks, BoundInnerClass
!
If you wanted ChildOfInner
to also get the outer
argument passed in to its __init__
, just decorate it with BoundInnerClass
instead of UnboundInnerClass
like so:
from boundinnerclass import BoundInnerClass
class Outer(object):
@BoundInnerClass
class Inner(object):
def __init__(self, outer):
self.outer = outer
@BoundInnerClass
class ChildOfInner(Inner.cls):
def __init__(self, outer):
super(Outer.ChildOfInner, self).__init__()
assert self.outer == outer
o = Outer()
i = o.ChildOfInner()
You can see more complex examples of using inheritance with BoundInnerClass
(and UnboundInnerClass
) in the recipe's test code.
Other Notes
- If you refer to a bound inner class directly from the outer class, rather than using the outer instance, you get the original class. This means that references to
Outer.Inner
are consistent, and it's a base class of all the bound inner classes. This also means that if you attempt to construct one without using an outer instance, you must pass in theouter
parameter by hand, just as you would have to pass in theself
parameter by hand when calling an unbound method. - If you refer to a bound inner class from an outer instance, you get a subclass of the original class.
- Bound classes are cached in the outer object, which both provides a small speedup and ensures that
isinstance
relationships are consistent. - You must not rename inner classes decorated with this recipe! The implementation of
BoundInnerClass
looks up the bound inner class in the outer object by name in several places. Adding aliases to bound inner classes is harmless, but the original attribute name must always work. - Bound inner classes from different objects are different classes. This is symmetric with bound methods; if you have two objects "a" and "b" that are instances of the same class,
a.BoundInnerClass != b.BoundInnerClass
, just asa.method != b.method
. - The binding only goes one level deep; if you had an inner class C inside another inner class B inside a class A, the constructor for C would be called with the B object, not the A object.
- Similarly, if you have a bound inner class B inside a class A, and another bound inner class D inside a class C, and D inherits from B, the constructor for D will be called with the B object but not the A object. When D calls
super().__init__
it'll have to fill in theouter
parameter by hand. - There is a race condition in the implementation: if you access a bound inner class through an outer instance from two separate threads, and the bound inner class was not previously cached, the two threads may get different (but equivalent) bound inner class objects, and only one of those instances will get cached on the outer object. This could lead to confusion and possibly cause bugs. For example, you could have two objects that would be considered equal if they were instances of the same bound inner class, but would not be considered equal if instantiated by different instances of that same bound inner class. There's an easy workaround for this problem: access the bound inner class from the
__init__
of the outer class, which should allow the code to cache the bound inner class instance before a second thread could ever get a reference to the outer object.
An Alternate Approach
I've contributed a second implementation of bound inner classes as a new recipe, here:
http://code.activestate.com/recipes/577623-bound-inner-classes-using-an-alternate-approach/
While that implementation is interesting, I think this original approach remains preferable for production code.
Revision History
Revision 1 was the original revision.
Revision 2 has been lost to the hoary mists of time.
Revision 3 removed the per-decorator cache mapping outer objects to the bound inner form of the decorated class. Instead, a reference to the bound inner class is stored in the object, occluding the original decorated class. This helps with object cleanup; storing this cache in the decorator meant the bound inner classes and the outer objects were effectively immortal. (They'd only get collected if you dropped all references to the outer object's class, and even then there was a long reference cycle.) Also, added warning about the surprising "bound inner subclasses don't appear to be children of the bound inner parent class" behavior.
Revision 4 cleaned up the interaction between bound inner classes and inheritance, by adding the "multiply inherit from bound inner version(s)" code and UnboundInnerSubclass
. Multiple inheritance now works cleanly, passing in the "outer" argument to the bound inner base classes is now automatic, and bound inner subclasses now inherit from their bound inner parent classes (which fixes some surprising isinstance
behavior from previous revisions).
Revision 5 changed BoundInnerClass
to keep only a weakref to the outer object. Without this change, BoundInnerClasses
created a reference cycle (o -> boundinnerclass, boundinnerclass -> o) which made object cleanup unnecessarily harder. Also, UnboundInnerSubclass
was renamed to UnboundInnerClass
; the former nomenclature was needlessly asymmetric. Third, all references to parent
were renamed to outer
. I think that's clearer, though it doesn't affect any of the interfaces. Finally, an explicit Zlib license was added to the source code. (The recipe is marked as being MIT license; ActiveState doesn't provide a choice for the Zlib license. Don't worry, Zlib is even less restrictive than MIT; it explicitly says that you don't need to preserve the copyright notice in compiled form.)
Revision 6 fixed a bug: if a bound inner class inherited from a class with the same name, the multiple-inheritance check would go into an infinite __get__
recursion loop and blow the stack. To fix it, the multiple-inheritance code now skips classes with the same name as the currently-being-bound class, as there's no way said class could be an inner class inside the same outer object.
Revision 7 made no bugfixes and added no features. It was purely a code readability edit, based on comments Alex made regarding the recipe in-person at PyCon 2011.
Revision 8 fixed an obscure bug: if class A defines bound inner class BIC, and class B descends from A but defines BIC to be something else (a method, a scalar, etc) but still uses class A's BIC (by calling super()
), and b is an object of type B, then BoundInnerClass
would overwrite b.BIC in with the bound inner version of A.BIC. BoundInnerClass
definitely shouldn't overwrite b.BIC in this case. Unfortunately that means we have nowhere to store the bound version of A.BIC, so this will break some isinstance()
uses--there's really no good solution here.