Recipe 6.6. Delegating Special Methods in Proxies
Credit: Gonçalo Rodrigues
Problem
In the new-style object model, Python
operations perform implicit lookups for special methods on the class
(rather than on the instance, as they do in the classic object
model). Nevertheless, you need to wrap new-style instances in proxies
that can also delegate a selected set of special methods to the
object they're wrapping.
Solution
You need to generate each proxy's class on the fly.
For example:
class Proxy(object):
"" base class for all proxies ""
def _ _init_ _(self, obj):
super(Proxy, self)._ _init_ _(obj)
self._obj = obj
def _ _getattr_ _(self, attrib):
return getattr(self._obj, attrib)
def make_binder(unbound_method):
def f(self, *a, **k): return unbound_method(self._obj, *a, **k)
# in 2.4, only: f._ _name_ _ = unbound_method._ _name_ _
return f
known_proxy_classes = { }
def proxy(obj, *specials):
''' factory-function for a proxy able to delegate special methods '''
# do we already have a suitable customized class around?
obj_cls = obj._ _class_ _
key = obj_cls, specials
cls = known_proxy_classes.get(key)
if cls is None:
# we don't have a suitable class around, so let's make it
cls = type("%sProxy" % obj_cls._ _name_ _, (Proxy,), { })
for name in specials:
name = '_ _%s_ _' % name
unbound_method = getattr(obj_cls, name)
setattr(cls, name, make_binder(unbound_method))
# also cache it for the future
known_proxy_classes[key] = cls
# instantiate and return the needed proxy
return cls(obj)
Discussion
Proxying and automatic delegation are a
joy in Python, thanks to the _ _getattr_ _ hook.
Python calls it automatically when a lookup for any attribute
(including a methodPython draws no distinction there) has not
otherwise succeeded.In the old-style (classic) object model, _ _getattr_
_ also applied to special methods that were looked up as
part of a Python operation. This required some care to avoid
mistakenly supplying a special method one didn't
really want to supply but was otherwise handy. Nowadays, the
new-style object model is recommended for all new code: it is faster,
more regular, and richer in features. You get new-style classes when
you subclass object or any other built-in type.
One day, some years from now, Python 3.0 will eliminate the classic
object model, as well as other features that are still around only
for backwards-compatibility. (See http://www.python.org/peps/pep-3000l for
details about plans for Python 3.0almost all changes will be
language simplifications, rather than new features.)In the new-style object model, Python operations
don't look up special methods at runtime: they rely
on "slots" held in class objects.
Such slots are updated when a class object is built or modified.
Therefore, a proxy object that wants to delegate some special methods
to an object it's wrapping needs to belong to a
specially made and tailored class. Fortunately, as this recipe shows,
making and instantiating classes on the fly is quite an easy job in
Python.In this
recipe, we don't use any advanced Python concepts
such as custom metaclasses and custom descriptors. Rather, each proxy
is built by a factory function proxy, which takes
as arguments the object to wrap and the names of special methods to
delegate (shorn of leading and trailing double underscores). If
you've saved the
"Solution"'s code
in a file named proxy.py somewhere along your
Python sys.path, here is how you could use it from
an interactive Python interpreter session:
>>> import proxySince _ _len_ _ is delegated,
>>> a = proxy.proxy([ ], 'len', 'iter')
# only delegate _ _len_ _ & _ _iter_ _
>>> a # _ _repr_ _ is not delegated
<proxy.listProxy object at 0x0113C370>
>>> a._ _class_ _
<class 'proxy.listProxy'>
>>> a._obj
[ ]
>>> a.append # all non-specials are delegated
<built-in method append of list object at 0x010F1A10>
len(a) works as expected:
>>> len(a)Since _ _iter_ _ is delegated,
0
>>> a.append(23)
>>> len(a)
1
for loops work as expected, as does intrinsic
looping performed by built-ins such as list,
sum, max, . . . :
>>> for x in a: print xHowever, since _ _getitem_ _ is
...
23
>>> list(a)
[23]
>>> sum(a)
23
>>> max(a)
23
not delegated, a
cannot be indexed nor sliced:
>>> a._ _getitem_ _Function proxy uses a
<method-wrapper object at 0x010F1AF0>
>>> a[1]
Traceback (most recent call last):
File "<interactive input>", line 1, in ?
TypeError: unindexable object
"cache" of classes it has
previously generated, the global dictionary
known_proxy_classes, keyed by the class of the
object being wrapped and the tuple of special
methods' names being delegated. To make a new class,
proxy calls the built-in type,
passing as arguments the name of the new class (made by appending
'Proxy' to the name of the class being wrapped),
class Proxy as the only base, and an
"empty" class dictionary (since
it's adding no class attributes yet). Base class
Proxy deals with initialization and delegation of
ordinary attribute lookups. Then, factory function
proxy loops over the names of specials to be
delegated: for each of them, it gets the unbound method from the
class of the object being wrapped, and sets it as an attribute of the
new class within a make_binder closure.
make_binder deals with calling the unbound method
with the appropriate first argument (i.e., the object being wrapped,
self._obj).Once it's done preparing a new class,
proxy saves it in
known_proxy_classes under the appropriate key.
Finally, whether the class was just built or recovered from
known_proxy_classes, proxy
instantiates it, with the object being wrapped as the only argument,
and returns the resulting proxy instance.
See Also
Recipe 6.5 for more
information about automatic delegation; Recipe 6.9 for another example of
generating classes on the fly (using a class
statement rather than a call to type).