Hello everyone,

While working on a program I encountered a situation where I'd
construct a largish data structure (a tree) from parsing a host of
files and would end up having to throw away parts of my newly built
tree if a file turned out to contain invalid data. My first thought was
'Well, you can always make a deep copy of your tree first, then add new
data to the copy and revert to the original if you need to.", but as
this tree can grow very big this is not exactly efficient.
So my second idea was to come up with a class that offers a very
limited degree of database-like behavior, meaning you can make changes
to the object and then decide whether you want to commit those changes
or roll them back to get back to the original. For example:

<CODE SNIPPET>

u = Unrollable()
u.someValue = 3.14
u.aString = 'Hi there'

# If we decide we want to keep those changes ...
u.commit()

# Or we could have reverted to the original. This would have restored
the state prior to the last call to commit() (or simply the state at
the beginning, if there hasn't been a call to commit yet).
#u.rollback()

</CODE SNIPPET>

The basic idea behind this is that each instance of the Unrollable
class keeps an internal dictionary (which, in lieu of a better name I'm
currently calling 'sand box') to which all changed attribute values are
saved (attribute changes are intercepted via __setattr__). Then, with a
call to commit(), all attributes are transferred to the instance's
__dict__ dictionary and hence become actual attributes per se.

Similarily, the rollback() function simply empties the contents of the
sand box without committing them to __dict__. The rollback() function
can work recursively, too, if passed some extra parameters. If so, it
walks either the sand box or the __dict__ (or both) and invokes the
rollback() function on any attribute members that are instances of the
Unrollable class or a derived class.

Finally, this works for 'private' attributes (i.e. names with two
leading underscores), too, as the __setattr__ implementation mangles
the name of the attribute if it detects a private name.

I'm posting this for 2 reasons. Firstly, I feel that I have finally
produced something that others _might_ find useful, too. Secondly,
since I'm still learning Python (yeah, even after 2 years), I would be
very grateful to hear people's criticisms. Are there things that could
be done more efficiently? Do you spot any grave errors? Does something
similar already exist that I should just have used instead? Right now
I'm rather pleased with my class, but if someone tells me there is
already something like this Python's library (and then it'll most
likely be more efficient anyway) then I'd of course rather use that.

Entire class definition + some test code attached to this post.

P.S. I __LOVE__ how something like this is just barely 70 lines of code
in Python!



class Unrollable( object ):
        """Provides a very simple commit/rollback system."""

        def __setattr__( self, attributeName, attributeValue ):
                """Changes the specified attribute by setting it to the passed 
value.
The change is only made to the sandbox and is not committed."""

                if attributeName.find( '__' ) == 0:
                        # Mangle name to make attribute private.
                        attributeName = '_' + self.__class__.__name__ + 
attributeName

                try:
                        theDict = self.__dict__[ '_Unrollable__dSandBox' ]
                except KeyError:
                        theDict = self.__dict__[ '_Unrollable__dSandBox' ] = {}


                theDict[ attributeName ] = attributeValue


        def __getattr__( self, attributeName ):
                """This method ensures an attribute can be accessed even when it
hasn't been committed yet (since it might not exist in the object
                itself yet)."""

                if attributeName.find( '__' ) == 0:
                        # Mangle name to make attribute private.
                        attributeName = '_' + self.__class__.__name__ + 
attributeName

                try:
                        theDict = self.__dict__[ '_Unrollable__dSandBox' ]
                except KeyError:
                        # Our sandbox doesn't exist yet, therefore the 
requested attribute
doesn't exist yet either.
                        raise AttributeError


                try:
                        return theDict[ attributeName ]
                except KeyError:
                        # No such attribute in our sandbox.
                        raise AttributeError

        def commitChanges( self ):
                """Commits the contents of the sandbox to the actual object. 
Clears
the sandbox."""
                while len( self.__dSandBox ) > 0:
                        key, value = self.__dSandBox.popitem()
                        self.__dict__[ key ] = value

        def unroll( self, bRecurseSandBox = True, bRecurseDict = False ):
                """Ditches all changes currently in the sandbox. Recurses all 
objects
in the instance itself and in its sandbox and, if
                they're unrollable instances themselves, invokes the unroll 
method on
them as well."""
                if bRecurseSandBox:
                        while len( self.__dSandBox ) > 0:
                                key, value = self.__dSandBox.popitem()

                                if isinstance( value, Unrollable ):
                                        value.unroll( bRecurseSandBox, 
bRecurseDict )
                else:
                        self.__dSandBox.clear()

                if bRecurseDict:
                        iterator = self.__dict__.itervalues()

                        while True:
                                try:
                                        nextVal = iterator.next()
                                except StopIteration:
                                        break

                                if isinstance( nextVal, Unrollable ):
                                        nextVal.unroll( bRecurseSandBox, 
bRecurseDict )

        def hasUncommittedChanges( self ):
                """Returns true if there are uncommitted changes, false 
otherwise."""
                return len( self.__dSandBox ) > 0




if __name__ == '__main__':
        # With a public attribute ...
        u = Unrollable()

        print 'Before.'

        try:
                theValue = u.theValue
        except AttributeError:
                print 'u does not have a theValue attribute yet.'
        else:
                print 'u.theValue is', theValue


        u.theValue = 3.147634

        print 'After set().'

        try:
                theValue = u.theValue
        except AttributeError:
                print 'u does not have a theValue attribute yet.'
        else:
                print 'u.theValue is', theValue

        u.commitChanges()

        print 'After commitChanges().'

        try:
                theValue = u.theValue
        except AttributeError:
                print 'u does not have a theValue attribute yet.'
        else:
                print 'u.theValue is', theValue

        print u.__dict__


        # With a private attribute ...
        class MyClass( Unrollable ):
                def accessPrivateAttr( self ):
                        try:
                                theValue = self.__theValue
                        except AttributeError:
                                print 'self does not have a __theValue 
attribute yet.'
                        else:
                                print 'self.__theValue is', theValue


        anObject = MyClass()

        print 'Before.'
        anObject.accessPrivateAttr()

        anObject.__theValue = 6.667e-11
        print 'After set().'
        anObject.accessPrivateAttr()

        #anObject.commitChanges()

        print 'After commitChanges().'
        anObject.accessPrivateAttr()




        anObject.subObject = Unrollable()
        anObject.subObject.aString = 'Yeeehaawww'

        print anObject.__dict__
        print anObject.subObject.__dict__
        
        anObject.unroll( True, True )
        
        print anObject.__dict__

-- 
http://mail.python.org/mailman/listinfo/python-list

Reply via email to