Welcome, guest | Sign In | My Account | Store | Cart

This is a recipe for new-style class proxies that can also delegate special methods.

Python, 61 lines
 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
"""
This module creates Proxies that can also trap and delegate magic names.

Usage example:
>>> from proxy import *
>>> a = proxy([], ["__len__", "__iter__"])
>>> a
<proxy.listProxy object at 0x0113C370>
>>> a.__class__
<class 'proxy.listProxy'>
>>> a._obj
[]
>>> a.append
<built-in method append of list object at 0x010F1A10>
>>> a.__len__
<bound method listProxy.<lambda> of <proxy.listProxy object at 0x0113C370>>
>>> len(a)
0
>>> a.__getitem__
<method-wrapper object at 0x010F1AF0>
>>> a[1]
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
TypeError: unindexable object
>>> list(a)
[]
"""

class Proxy(object):
    """The Proxy base class."""

    def __init__(self, obj):
        """The initializer."""
        super(Proxy, self).__init__(obj)
        #Set attribute.
        self._obj = obj
        
    def __getattr__(self, attrib):
        return getattr(self._obj, attrib)


#Auxiliary getter function.
def getter(attrib):
    return lambda self, *args, **kwargs: getattr(self._obj, attrib)(*args, **kwargs)


def proxy(obj, names):
    """Factory function for Proxies that can delegate magic names."""
    #Build class.
    cls = type("%sProxy" % obj.__class__.__name__,
               (Proxy,),
               {})
    #Add magic names.
    for name in names:
        #Filter magic names.
        if name.startswith("__") and name.endswith("__"):
            if hasattr(obj.__class__, name):
                #Set attribute.
                setattr(cls, name, getter(name))
    #Return instance.
    return cls(obj)

Delegation is a joy in Python. All because of the __getattr__ hook that can delegate missing attributes to another object. Using old style classes

>>> class Proxy:
...     def __init__(self, obj):
...         self._obj = obj
...     def __getattr__(self, attrib):
...         return getattr(self._obj, attrib)
...
>>> a = Proxy([])
>>> list(a)
[]
>>> a.append(1)
>>> a._obj
[1]

Life was good. But then new-style classes entered the scene. If one tries to do the same with new-style classes

>>> class Proxy(object):
...     def __init__(self, obj):
...         self._obj = obj
...     def __getattr__(self, attrib):
...         return getattr(self._obj, attrib)
...
>>> a = Proxy([])
>>> list(a)
Traceback (most recent call last):
  File "", line 1, in ?
TypeError: iteration over non-sequence

And that's a :-(

What's happening here is that list is calling iter(a), and iter(a) does not ask for a.__iter__. Instead, it looks for __iter__ in the class dictionary, and then all the way through the superclasses. Notice that it is not even the case that iter(a) fetches __iter__ by getattr(a.__class__, "__iter__") as the following simple metaclass will show

>>> class MetaGetter(type):
...     def __getattr__(cls, attrib):
...         if attrib == "__iter__":
...             return lambda self: iter([])
...
>>> class YetAnotherProxy(object):
...     __metaclass__ = MetaGetter
...     def __init__(self, obj):
...         self._obj = obj
...
>>> a = YetAnotherProxy([])
>>> a.__class__.__iter__
 at 0x01123E30>
>>> list(a.__class__.__iter__(a))
[]
>>> iter(a)
Traceback (most recent call last):
  File "", line 1, in ?
TypeError: iteration over non-sequence
>>>

So, the only way to have our proxies to respond to syntactic sugar is to inject the magic methods directly. We do that by building a subclass of Proxy on the fly, where for each magic name supplied in an argument list, we look for the corresponding magic name in the wrapped object, and then build the method by delegation. The return value is then an instance of this class.

I Love Python! G. Rodrigues

2 comments

Shane Holloway 11 years, 10 months ago  # | flag

The best part about this recipe is the explanation of why it doesn't work as expected! Thanks!

Tal Einat 11 years ago  # | flag

There is a downside to this approach: The proxies generated will not be pickle-able, since they are instances of a dynamically defined class.

Created by Gonçalo Rodrigues on Tue, 18 Nov 2003 (PSF)
Python recipes (4591)
Gonçalo Rodrigues's recipes (9)

Required Modules

  • (none specified)

Other Information and Tasks