3

I am trying to come up with a way to keep track of various re-incarnations of an object in Python. For example, I would like to have a class for, say, a room. This room could start its existence as a 'kitchen', and then, at any given point in time, become a 'study'. I want to be able to track (or log) all these various phases in the life of my room. So, I came up with this, and am wondering if there is a better way to do this. Or maybe I'm missing something. From what I can tell, though, it appears to work for my purposes.

class room():
    def __init__(self,current="Any", history = []):
        self.current = childroom(current)
        self.history = history
        self.history.append(self.current)

    def changeroom(self,current = "New"):
        self.current = childroom(current)
        self.history.append(self.current)


class childroom(room):
    def __init__(self,name="Any"):
        self.name = name    

When I use this code...

>>> myroom = room("kitchen")
>>> myroom.changeroom("study")
>>> myroom
<__main__.room instance at 0x0000000003139208>
>>> myroom.history
[<__main__.childroom instance at 0x0000000003139288>, <__main__.childroom instance at 0x00000000023F4AC8>]
>>> myroom.history[0].name
'kitchen'
Arne
  • 385
  • 1
  • 6
  • 15
  • 1
    Welcome to StackOverflow! Right now, it appears that your question is "Is there a better way to do this?" Of course, what is a "better" way to do anything is pretty subjective (one way might be easier to code with, another might run faster, another take up less memory, another would be well suited to persisting to a database). As such, it would be impossible for there to be a single answer to this question, so StackOverflow is not the best place to get that sort of feedback. If you are looking for general comments on your code, please look at codereview.stackexchange.com. – Mark Hildreth Oct 01 '13 at 16:36
  • 3
    Aside: `def __init__(self,current="Any", history = []):` means that each `room` which isn't passed a value for `history` upon initialization will share the *same* history. IOW, one `history` list is constructed when the `def` is executed, not each time an object is made. Read [this question](http://stackoverflow.com/questions/1132941/least-astonishment-in-python-the-mutable-default-argument) about mutable default arguments for the details. – DSM Oct 01 '13 at 16:38
  • If history is saved in a list, do you really need to track self.current individually? It should always be the last item appended. – tom Oct 01 '13 at 19:29
  • Thanks for the welcome, Mark. So would it have been better to ask: how do I ...? DSM, you're right, it would probably be better to say: class room(): def __init__(self,current="Any"). No need to pass a default history, and no need to pass history to __ini__() anyway. --- tom, I am not sure what you're saying. In order for me to store an object with a name in a list for future reference, I need to assign it a name, which is the argument passed to the changeroom() method. Unless you're saying I should not be using 'name' in childroom, but rather 'current'. – Arne Oct 02 '13 at 13:05

2 Answers2

3

I personally would implement it like this:

#! /usr/bin/python3
import copy

class Room:
    def __init__ (self, state = 'Initial'):
        self.state = state
        self.history = []

    def morph (self, state):
        clone = copy.deepcopy (self)
        self.state = state
        self.history.append (clone)

Keep in mind, that I don't know if your real setup has some features that restrict deep copying.

This yields:

>>> r = Room ('Kitchen')
>>> r.morph ('Loo')
>>> r.morph ('Spaceship')
>>> r.state
'Spaceship'
>>> [a.state for a in r.history]
['Kitchen', 'Loo']
>>> [type (a) for a in r.history]
[<class 'test.Room'>, <class 'test.Room'>]

I guess normally you don't need to save the whole state of an object, but only attributes which are worth tracking. You could pack this behaviour into a decorator along these lines:

#! /usr/bin/python3

import datetime
import copy

class Track:
    def __init__ (self, *args, saveState = False):
        self.attrs = args
        self.saveState = saveState

    def __call__ (self, cls):
        cls._track = []
        this = self

        oGetter = cls.__getattribute__
        def getter (self, attr):
            if attr == 'track': return self._track
            if attr == 'trackTrace': return '\n'.join ('{}: "{}" has changed to "{}"'.format (*t) for t in self._track)
            return oGetter (self, attr)
        cls.__getattribute__ = getter

        oSetter = cls.__setattr__
        def setter (self, attr, value):
            if attr in this.attrs:
                self._track.append ( (datetime.datetime.now (), attr, copy.deepcopy (value) if this.saveState else value) )
            return oSetter (self, attr, value)
        cls.__setattr__ = setter

        return cls

Now we can use this decorator like this:

@Track ('holder')
class Book:
    def __init__ (self, title):
        self.title = title
        self.holder = None
        self.price = 8

class Person:
    def __init__ (self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName

    def __str__ (self):
        return '{} {}'.format (self.firstName, self.lastName)

r = Book ('The Hitchhiker\'s Guide to the Galaxy')
p = Person ('Pedro', 'Párramo')
q = Person ('María', 'Del Carmen')
r.holder = p
r.price = 12
r.holder = q
q.lastName = 'Del Carmen Orozco'
print (r.trackTrace)

If called with @Track ('holder'), it yields:

2013-10-01 14:02:43.748855: "holder" has changed to "None"
2013-10-01 14:02:43.748930: "holder" has changed to "Pedro Párramo"
2013-10-01 14:02:43.748938: "holder" has changed to "María Del Carmen Orozco"

If called with @Track ('holder', 'price'), it yields:

2013-10-01 14:05:59.433086: "holder" has changed to "None"
2013-10-01 14:05:59.433133: "price" has changed to "8"
2013-10-01 14:05:59.433142: "holder" has changed to "Pedro Párramo"
2013-10-01 14:05:59.433147: "price" has changed to "12"
2013-10-01 14:05:59.433151: "holder" has changed to "María Del Carmen Orozco"

If called with @Track ('holder', saveState = True), it yields:

2013-10-01 14:06:36.815674: "holder" has changed to "None"
2013-10-01 14:06:36.815710: "holder" has changed to "Pedro Párramo"
2013-10-01 14:06:36.815779: "holder" has changed to "María Del Carmen"
Hyperboreus
  • 31,997
  • 9
  • 47
  • 87
  • Hyperboreus, thanks for your output - wow - will need some time to digest. There is a lot of stuff in there I don't understand. One reason I wanted to store the entire object, which I should have mentioned, is that ultimately, there will be too many attributes that change with each state change that I thought storing a snapshot of the whole object would be easiest. Will need to investigate 'deepcopy'! – Arne Oct 02 '13 at 13:08
0

This sounds like a case for a facade pattern - have an inner room object which captures current state, and use __setattr__ on the facade to capture assignments, archive the previous state (inner object), and create a new one with the updated property value. You'll want to override __getattr__ to delegate to the inner object, which will also have the appropriate methods for anything else.

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • Marcin, that sounds more like what I set out to do initially and then couldn't get to work. I will give it some more thought. – Arne Oct 02 '13 at 13:07