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