Wrapper objects

Nick Coghlan ncoghlan at iinet.net.au
Fri Dec 10 18:41:55 CET 2004


Kent Johnson wrote:
> Nick Coghlan wrote:
> 
>> Simon Brunning wrote:
>>
>>> This work - 
>>> <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52295>?
>>>
>>
>> Only for old-style classes, though. If you inherit from object or 
>> another builtin, that recipe fails.
> 
> 
> Could you explain, please? I thought __getattr__ worked the same with 
> new- and old-style classes?

Looking at the recipe more closely, I believe you are correct - the behaviour 
shouldn't change much between old and new style classes (the main difference 
being that the new-style version is affected by descriptors, along with the 
other things which may prevent __getattr__ from being invoked in either sort of 
class).

However, all that means is that the recipe wouldn't help the OP even with a 
classic class. In neither case will implicit invocation find the correct methods 
on the object we are delegating to.

The trick has to do with the way special values are often looked up by the 
Python interpreter.

Every class object contains entries that correspond to all the magic methods 
that Python knows about (in CPython, these are function pointers inside a C 
structure, FWIW).

When looking for a special method, the interpreter may simply check the relevant 
entry in the class object directly - if it's empty, it assumes the magic method 
is not defined and continues on that basis.

A simple example:

   class foo:
     def __init__(self):
       print "Hi there!"

When a class object is built from this definition, the "def __init__" line 
actually means two things:
   1. Declare a standard Python function called '__init__' in the class 'foo'
   2. Populate the appropriate magic method entry in class 'foo'

When overriding __getattribute__ only, step 2 never happens for most of the 
magic methods, so, as far as the interpreter is concerned, the class may provide 
access to an attribute called "__add__" (via delegation), but it does NOT 
provide the magic function "__add__".

In order to have the delegation work as expected, Python has to be told which 
magic method entries should be populated (there is no sensible way for Python to 
guess which methods you intend to delegate - delegating __init__ or 
__getattribute__ is almost certainly insane, but what about methods like 
__call__ or __getattr__? __repr__ and __str__ pose interesting questions, too)

A nice way to do this is with a custom metaclass (thanks to Bengt for inspiring 
this design - note that his version automatically delegates everything when you 
call wrapit, but you have to call wrapit for each class you want to wrap, 
whereas in this one you spell out in your wrapper class which methods are 
delegated, but that class can then wrap just about anything).

wrapper.py:
=================
# A descriptor for looking up the item
class LookupDescr(object):
     def __init__(self, name):
         self._name = name

     def __get__(self, inst, cls=None):
         if inst is None:
             return self
         # Look it up in the Python namespace
         print self._name # Debug output
         return inst.__getattr__(self._name)

# Our metaclass
class LookupSpecialAttrs(type):
     """Metaclass that looks up specified 'magic' attributes consistently

        __lookup__: a list of strings specifying method calls to look up
     """

     def __init__(cls, name, bases, dict):
         # Create the 'normal' class
         super(LookupSpecialAttrs, cls).__init__(name, bases, dict)
         # Now create our looked up methods
         if (hasattr(cls, "__lookup__")):
             for meth in cls.__lookup__:
                 setattr(cls, meth, LookupDescr(meth))


# Simple wrapper
class Wrapper(object):
     """Delegates attribute access and addition"""
     __metaclass__ = LookupSpecialAttrs
     __lookup__ = ["__add__", "__radd__", "__str__", "__int__"]

     def __init__(self, obj):
         super(Wrapper, self).__setattr__("_wrapped", obj)

     def __getattr__(self, attr):
         wrapped = super(Wrapper, self).__getattribute__("_wrapped")
         return getattr(wrapped, attr)

     def __setattr__(self, attr, value):
         setattr(self._wrapped, attr, value)

=================

Using our new wrapper type:
=================
.>>> from wrapper import Wrapper
.>>> x = Wrapper(1)
.>>> x + 1
__add__
2
.>>> 1 + x
__radd__
2
.>>> print x
__str__
1
.>>> x + x
__add__
__add__
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for +: 'Wrapper' and 'Wrapper'
.>>> x = wrapper.Wrapper("Hi")
.>>> x + " there!"
__add__
'Hi there!'
.>>> "Wrapper says " + x
__radd__
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
   File "wrapper.py", line 11, in __get__
     return inst.__getattr__(self._name)
   File "wrapper.py", line 40, in __getattr__
     return getattr(wrapped, attr)
AttributeError: 'str' object has no attribute '__radd__'
.>>> x + x
__add__
Traceback (most recent call last):
   File "<stdin>", line 1, in ?
TypeError: cannot concatenate 'str' and 'Wrapper' objects
=================

So close! What's going wrong here? Well, it has to do with the fact that, when 
developing new types, the onus is on the author of the type to play well with 
others (e.g. accepting builtin types as arguments to operations).

Even wrapping '__int__' and '__str__' hasn't helped us - the builtin add methods 
don't try to coerce either argument. Instead, they fail immediately if either 
argument is not of the correct type. (Ditto for many other operations on builtin 
types)

That's why in the examples that worked, it is the method of our wrapper object 
that was invoked - after the delegation, both objects were the correct type and 
the operation succeeded.

For the 'two wrapper objects' case, however, when we do the delegation, 
regardless of direction, the 'other' argument is a Wrapper object. So the 
operational fails. And strings don't have __radd__, so the operation with our 
wrapper on the right failed when we were wrapping a string.

However, some judicious calls to str() and int() can fix all those 'broken' cases.

Fixing the broken cases:
=================
.>>> x = Wrapper("Hi")
.>>> "Wrapper says " + str(x)
__str__
'Wrapper says Hi'
.>>> str(x) + str(x)
__str__
__str__
'HiHi'
.>>> x = Wrapper(1)
.>>> int(x) + int(x)
__int__
__int__
2
=================

Methinks I'll be paying a visit to the cookbook this weekend. . .

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at email.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://boredomandlaziness.skystorm.net



More information about the Python-list mailing list