[Python-Dev] Writing a mutable object problem with __setattr__

Guido van Rossum guido@python.org
Tue, 25 Feb 2003 09:50:12 -0500


> I am trying to create a mutable object for a persistent object 
> database, and I've ran into a problem. The semantics are:
> 
> Object gets created as a 'Swizzle'. Swizzle is a stub that 
> intercepts any attempt to access object properties. When code 
> tries to access any of swizzle's properties, the swizzle should 
> automatically mutate into the stored object, by changing its 
> class and properties.

This is a common pattern; Zope (really ZODB) uses this and calls the
incomplete objects "ghosts".  Zope doesn't change the __class__
though.

> With the __set/get/del/attr__ traps this seemed feasible in 
> Python. But it turns out to be trickier than I've thought, my 
> naive implementation is not working.
> 
> What happens is:
> swizzle.__getattr__() gets called
>    calls swizzle.load()
>      swizzle.__class__ = stored_class
>      swizzle.__dict__ = stored_data
>    calls getattr(self, attr_name)
>    calls swizzle.__getattr__() gets called
>      and I enter an infinite loop and blow the stack
> 
> Since swizzle's __class__ and __dict__ have changed, shouldn't 
> getattr use the new class to get attributes?

That's what I woul'd have thought too.  Maybe the problem is that
you're really requesting a non-existent attribute?  Do you know which
attribute name is being requested?  That's the first information you
need.

> Why this is puzzling is that if I call swizzle.load() directly 
> (bypassing __getattr__ trap), and then try referencing swizzle's 
> fields, everything works. The object mutates successfully, and 
> __getattr__ never gets called again.
> 
> Any hints on what might be going wrong? Hopefully others have 
> struggled with the same problem before me.
> 
> Details of Python's class implementation and method dispatch 
> would also be interesting. I've read Guido's Unifying Types 
> essay, but I think I lack Python history required for its full 
> understanding.
> 
> I've tried many different approaches:
> - Swizzle inheriting from object (so that I can call super 
> methods directly). This one would not let me assign into the 
> __class__

In Python 2.2.2 you can set __class__, as long as __class__ has a
compatible instance lay-out (at the C implementation level).

With new-style objects, you can use __getattribute__ instead of
__getattr__ for more control (and also more opportunities to blow the
stack :-).

> - Swizzle calling different combinations of 
> self.__setattr/setattr/setattribute

I don't understand -- these names don't exist.

> Here is my current code.
> 
> Thanks in advance,
> 
> Aleks
> 
> class Swizzle(object):
>      """ A swizzle is a placeholder for an object that has not
>      been loaded. It mutates into the loaded object upon access
>      TODO: locking
>      """
>      def __init__(self, oid):
>          self.__setattr__('oid', oid)
> 
>      def __getattr__(self, name):
>          print "swizzle getattr called"
>          # oid is the only attribute that does not cause a load
>          if (name == 'oid'):
>              return self.__dict__[name]
>          self.load()
>          return getattr(self,name)
> 
>      def __setattr__(self, name, value):
>          """ setattr passes class & dict because these are called
>          when we are mutating the object """
>          print "swizzle setattr called"
>          if (name == '__class__' or name == '__dict__' or name == 
> 'oid'):
>              self.__dict__[name] = value
>              return

Ah, here's the catch.  You can't implement assignment to __dict__ or
__class__ by doing "self.__dict__[name] = value".

Use new-style classes and you'll be much happier: you can invoke the
superclass __setattr__ to do the magic.

>          self.load()
>          return setattr(self,name, value)
> 
>      def __delattr__(self, name):
>          print "swizzle delattr called"
>          self.load()
>          return delattr(self, name)
> 
>      def load(self):
>          """ TODO LOCK"""
>          print "Trying to load ", self.oid
>          RepositoryManager.manager.load(self)
> 
> def RepositoryManager.load(anItem)
>   anItem.__dict__ = getProperties(anItem.oid)
>   anItem.__class__ = getClass(anItem.oid)

--Guido van Rossum (home page: http://www.python.org/~guido/)