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