Wrapper objects
Nick Coghlan
ncoghlan at iinet.net.au
Fri Dec 10 12:41:55 EST 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