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 | [EMAIL PROTECTED] | Brisbane, Australia --------------------------------------------------------------- http://boredomandlaziness.skystorm.net -- http://mail.python.org/mailman/listinfo/python-list