Descriptor/Decorator challenge

Arnaud Delobelle arnodel at googlemail.com
Mon Mar 5 13:59:02 EST 2007


On 5 Mar, 14:38, "Arnaud Delobelle" <arno... at googlemail.com> wrote:
> On 5 Mar, 07:31, "Raymond Hettinger" <pyt... at rcn.com> wrote:
>
> > I had an idea but no time to think it through.
> > Perhaps the under-under name mangling trick
> > can be replaced (in Py3.0) with a suitably designed decorator.
> > Your challenge is to write the decorator.
> > Any trick in the book (metaclasses, descriptors, etc) is fair game.
>
> I had some time this lunchtimes and I needed to calm my nerves so I
> took up your challenge :)
> Here is my poor effort.  I'm sure lots of things are wrong with it but
> I'm not sure I'll look at it again.

Well in fact I couldn't help but try to improve it a bit. Objects now
don't need a callerclass attribute, instead all necessary info is
stored in a global __callerclass__. Bits that didn't work now do.

So here is my final attempt, promised. The awkward bits are:
    * how to find out where a method is called from
    * how to resume method resolution once it has been established a
local method has to be bypassed, as I don't know how to interfere
directly with mro.

Feedback of any form is welcome (though I prefer when it's polite :)

--------------------
from types import MethodType, FunctionType

class IdDict(object):
    def __init__(self):
        self.objects = {}
    def __getitem__(self, obj):
        return self.objects.get(id(obj), None)
    def __setitem__(self, obj, callerclass):
        self.objects[id(obj)] = callerclass
    def __delitem__(self, obj):
        del self.objects[id(obj)]

# This stores the information about from what class an object is
calling a method
# It is decoupled from the object, better than previous version
# Maybe that makes it easier to use with threads?
__callerclass__ = IdDict()

# The purpose of this class is to update __callerclass__ just before
and after a method is called
class BoundMethod(object):
    def __init__(self, meth, callobj, callerclass):
        self.values = meth, callobj, callerclass
    def __call__(self, *args, **kwargs):
        meth, callobj, callerclass = self.values
        if callobj is None and args:
            callobj = args[0]
        try:
            __callerclass__[callobj] = callerclass
            return meth(*args, **kwargs)
        finally:
            del __callerclass__[callobj]

# A 'normal' method decorator is needed as well
class method(object):
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, objtype=None):
        return BoundMethod(MethodType(self.f, obj, objtype), obj,
self.defclass)

class LocalMethodError(AttributeError):
    pass

# The suggested localmethod decorator
class localmethod(method):
    def __get__(self, obj, objtype=None):
        callobj = obj or objtype
        defclass = self.defclass
        if __callerclass__[callobj] is defclass:
            return MethodType(self.f, obj, objtype)
        else:
            # The caller method is from a different class, so look for
the next candidate.
            mro = iter((obj and type(obj) or objtype).mro())
            for c in mro: # Skip all classes up to the localmethod's
class
                if c == defclass: break
            name = self.name
            for base in mro:
                if name in base.__dict__:
                    try:
                        return base.__dict__[name].__get__(obj,
objtype)
                    except LocalMethodError:
                        continue
        raise LocalMethodError, "localmethod '%s' is not accessible
outside object '%s'" % (self.name, self.defclass.__name__)

class Type(type):
    def __new__(self, name, bases, attrs):
        # decorate all function attributes with 'method'
        for attr, val in attrs.items():
            if type(val) == FunctionType:
                attrs[attr] = method(val)
        return type.__new__(self, name, bases, attrs)
    def __init__(self, name, bases, attrs):
        for attr, val in attrs.iteritems():
            # Inform methods of what class they are created in
            if isinstance(val, method):
                val.defclass = self
            # Inform localmethod of their name (in case they have to
be bypassed)
            if isinstance(val, localmethod):
                val.name = attr

class Object(object):
    __metaclass__ = Type

# Here is your example code

class A(Object):
    @localmethod
    def m(self):
        print 'A.m'
    def am(self):
        self.m()

class B(A):
    @localmethod
    def m(self):
        print 'B.m'
    def bm(self):
        self.m()

m = B()
m.am()     # prints 'A.m'
m.bm()     # prints 'B.m'
# Added:
B.am(m)    # prints 'A.m'
B.bm(m)    # prints 'B.m'
m.m()      # LocalMethodError (which descends from AttributeError)

# Untested beyond this particular example!
--------------------

--
Arnaud





More information about the Python-list mailing list