Seite 1 von 1

Änderungen in einem Dict mitbekommen...

Verfasst: Donnerstag 4. Januar 2007, 11:36
von jens
Ich habe eine Klasse, die von dict ableitet. Ich hätte gern innerhalb der Klasse gewusst, ob sich Daten geändert haben.

Ich dachte mit ich überschreibe einfach die __setitem__ Methode und setzte da einen Wert um... Aber so einfach ist das nicht:

Code: Alles auswählen

fake_db = {
    1 : {"jo": "Daten für Modul 1"},
    2 : {
        "jup": "Daten für Modul 2",
        "data": {},
    },
}

class PluginConfig(dict):
    _cache = {}

    def __init__(self, module_id):
        self.module_id = module_id

        self.modified = False

    def init2(self):
        if self.module_id in self._cache:
            # Daten sind schon im cache
            print "Aus dem cache"
            dict.__init__(self, self._cache[self.module_id])
            return

        # Daten sind noch nicht im cache -> aus db holen
        print "aus der DB"
        data = fake_db[self.module_id]

        # In cache eintragen
        self._cache[self.module_id] = data
        dict.__init__(self, data)

    def __setitem__(self, key, value):
        print "setitem!"
        self.modified = True
        dict.__setitem__(self, key, value)

    def commit(self):
        print "commit!"
        print "self.modified:", self.modified
        print dict.__str__(self)
        print self._cache[self.module_id]



p = PluginConfig(1)
p.init2()
print p

print "-"*79

p = PluginConfig(1)
p.init2()
print p
p["jo"] = "neu" # Hier geht es, natürlich
p.commit()

print "-"*79

p = PluginConfig(2)
p.init2()
print p
p["data"]["neu"] = 1 # Hier nicht
p.commit()
Wie man sehen kann, wird bei p["data"]["neu"] = 1 nicht __setitem__ aufgerufen...

Wie könnte ich das Lösen?

EDIT: Eine Lösung die mir einfällt: Ich mache eine deepcopy und vergleichen dann... :?

Verfasst: Donnerstag 4. Januar 2007, 11:42
von jens
Jup, mit deepcopy geht's:

Code: Alles auswählen

import copy

fake_db = {
    1 : {"jo": "Daten für Modul 1"},
    2 : {
        "jup": "Daten für Modul 2",
        "data": {},
    },
}

class PluginConfig(dict):
    _cache = {}

    def __init__(self, module_id):
        self.module_id = module_id

    def init2(self):
        if self.module_id in self._cache:
            # Daten sind schon im cache
            print "Aus dem cache"
            dict.__init__(self, self._cache[self.module_id])
            return

        # Daten sind noch nicht im cache -> aus db holen
        print "aus der DB"
        data = fake_db[self.module_id]

        # In cache eintragen
        self._cache[self.module_id] = copy.deepcopy(data)
        dict.__init__(self, data)

    def commit(self):
        print "commit!"
        if self._cache[self.module_id] != dict(self):
            print "modified!"
        else:
            print "not modified"

        print dict.__str__(self)
        print self._cache[self.module_id]



p = PluginConfig(1)
p.init2()
print p
p.commit()

print "-"*79

p = PluginConfig(1)
p.init2()
print p
p["jo"] = "neu" # Hier geht es, natürlich
p.commit()

print "-"*79

p = PluginConfig(2)
p.init2()
print p
p["data"]["neu"] = 1 # Hier nicht
p.commit()
Kennt jemand noch eine andere Lösung???

Verfasst: Donnerstag 4. Januar 2007, 11:45
von cracki
p['data']['neu'] = irgendwas

dann wird nicht p.__setitem__ aufgerufen, sondern p.__getitem__('data').__setitem__('neu', irgendwas)

Verfasst: Donnerstag 4. Januar 2007, 11:48
von jens
Da hast du wohl recht... Aber das irgendwie "mit zu Protokollieren" ist wohl etwas schwierig...

Da die Daten eh als pickle in die DB wandern, kann der Programierer jede Form von picklebaren Daten ablegen...

Ich nehme erstmal die deepcopy Version...

EDIT: Warum mir deepcopy nicht gefällt: Ich hab es zwar nicht nachgemessen, aber ich kann mir vorstellen, das die deepcopy-Aktion einige Zeit kostet.
Allerdings dürfte das pickeln und zurück schreiben in die DB mehr Zeit kosten. Zumal das ändern der Daten viel weniger vorkommen sollte, als das lesen der Daten ;)

Verfasst: Donnerstag 4. Januar 2007, 12:04
von CM
Hoi Jens,

ein paar Kommentare, vielleicht hilft es ja:
jens hat geschrieben:EDIT: Warum mir deepcopy nicht gefällt: Ich hab es zwar nicht nachgemessen, aber ich kann mir vorstellen, das die deepcopy-Aktion einige Zeit kostet.
Meiner Erfahrung nach: Ja, das tut es. Natürlich, das ist ja auch zu erwarten. Allerdings sind die Objekte, bei denen ich das spüre auch locker 2 bis 4 Mb groß. Einen Benchmark habe ich zwar nicht, aber selbst in dem geschilderten Fall, fällt es mit < 1 s pro Objekt (AMD 64 bit auf einem Acer Aspire 5020) nicht ins Gewicht.
jens hat geschrieben: Allerdings dürfte das pickeln und zurück schreiben in die DB mehr Zeit kosten. Zumal das ändern der Daten viel weniger vorkommen sollte, als das lesen der Daten ;)
Kommt natürlich drauf an. Aber kann Deine DB-Software (ich weiß leider nicht aus alten Threads, was Du nutzt) u. U. Änderungen verfolgen oder zumindest irgendwie anzeigen?

Besten Gruß im neuen Jahr,
Christian

Verfasst: Donnerstag 4. Januar 2007, 12:11
von jens
Ich nutzte MySQL... Aber warum soll mir das helfen Änderungen aufzuzeigen???

Also ich will vor dem updaten der DB Daten prüfen, ob das überhaupt notwendig ist.
Der einfachste weg wäre es wohl die Daten immer zu picklen und in die DB zu schreiben :)
Das dürfte IMHO mehr Performance kosten, als mit hilfe von deepcopy nachzuprüfen ob ein DB-Update überhaupt sein muß ;)

Die Frage ist, ob es was schnelleres als die deepcopy-Lösung gibt...

Verfasst: Donnerstag 4. Januar 2007, 13:44
von rayo
Hi

Also wenn du nur Dictionaries/Listen hast, kannst du es so lösen:

Code: Alles auswählen

class WatchedDict(dict):
    modified = False

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            value = WatchedDict(value)
        WatchedDict.modified = True
        dict.__setitem__(self, key,value)

    def save(self):
        if WatchedDict.modified:
            #speichern
            WatchedDict.modified = False
        else:
            print 'Dict wurde nicht veraendert'
So wird einfach jedes Dictionary in ein WatchedDict umgewandelt und lauscht somit auf jedem __setitem__.

Gruss

Verfasst: Donnerstag 4. Januar 2007, 13:56
von jens
Hm... Naja, ich könnte mir schon vorstellen es auf Dicts und Listen zu beschränken... Warum nicht...

Aber müsste ich nicht dann bei save() die Datenstruktur rekursiv durchsuchen, ob nicht irgendwo was geändert wurde???

Verfasst: Freitag 5. Januar 2007, 09:46
von rayo
Hi

Nein, wie du siehst verwende ich WatchedDict.modified, eine Klassenvariable (sprich die gibts nur einmal). Diese wird von jeder Instanz aus geändert.

Also ist WatchedDict.modified == True wenn irgendwo in der Struktur ein __setitem__ aufgerufen wurde.

Gruss

Verfasst: Freitag 5. Januar 2007, 10:17
von jens
Ne, so einfach ist das nicht... Ich hab mal einen Test gemacht:

Code: Alles auswählen

class WatchedDict(dict):
    modified = False

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            value = WatchedDict(value)
        WatchedDict.modified = True
        dict.__setitem__(self, key,value)

    def save(self):
        if WatchedDict.modified:
            #speichern
            WatchedDict.modified = False
            print 'Dict wurde veraendert'
        else:
            print 'Dict wurde nicht veraendert'

init_data = {"dict": {"liste":[1,2,3], "test1": 1}, "test2": "jup"}

d = WatchedDict(init_data)
print d
print "-"*79

d["dict"]["liste"][1] = 9
print d
d.save() # unbemerkt
print

d["dict"]["test1"] = "yes"
print d
d.save() # unbemerkt
print

d["test"] = [1,2]
print d
d.save() # OK
print
Ausgabe:

Code: Alles auswählen

{'test2': 'jup', 'dict': {'liste': [1, 2, 3], 'test1': 1}}
-------------------------------------------------------------------------------
{'test2': 'jup', 'dict': {'liste': [1, 9, 3], 'test1': 1}}
Dict wurde nicht veraendert

{'test2': 'jup', 'dict': {'liste': [1, 9, 3], 'test1': 'yes'}}
Dict wurde nicht veraendert

{'test': [1, 2], 'test2': 'jup', 'dict': {'liste': [1, 9, 3], 'test1': 'yes'}}
Dict wurde veraendert
Ist auch klar, warum es so nicht geht... Das dict innerhalb der init_data ist halt ein normales dict und kein WatchedDict...

Ich müsste also die __init__ Methode vom WatchedDict überschreiben und dann die Datenstruktur rekursiv durchgehen und alle normalen dicts in ein WatchedDict verwandeln. Das gleiche müsste ich zumindest auch noch für Listen machen...

Wenn ich das so mache, bin ich fast schon bei der deepcopy Variante. Wobei in dabei eine Kopie aller Daten existiert und bei der WatchedDict keine doppelten Daten im Speicher sind.

Verfasst: Freitag 5. Januar 2007, 19:32
von Luzandro
Nachdem mich das jetzt auch interessiert hat, hab ich einen allgemeineren Wrapper geschrieben, der prinzipiell funktionieren sollte - ob es wirklich so sinnvoll ist, weiß ich nicht :)
Grundsätzlich ist die Idee, dass bei jedem getItem/Attribute das erhaltene Objekt wieder durch ein ObserverObjekt ersetzt wird, welches eine Referenz auf das erste hat und bei jedem setItem/Attribute wird der modified-Status von diesem Root-Objekt auf True gesetzt
Was allerdings natürlich nicht erkannt wird ist, wenn ein enthaltenes Objekt direkt geändert wird ohne Aufruf der get-Methoden

Code: Alles auswählen

class ObservedObject(object):
    def __init__(self, obj, rootObject=None):
        self._obj = obj
        if rootObject is None:
            self._root = self
        else:
            self._root = rootObject
        self.modified = False

    def __setattr__(self, name, value):
        if name.startswith('_') or name == 'modified':
            object.__setattr__(self, name, value)
        else:
            object.__setattr__(self._obj, name, value)
            self._root.modified = True

    def __getattribute__(self, name):
        if name.startswith('_') or name == 'modified':
            return object.__getattribute__(self, name)
        else:
            value = object.__getattribute__(self._obj, name)
            if not isinstance(value, ObservedObject):
                value = ObservedObject(value, rootObject=self._root)
                object.__setattr__(self._obj, name, value)
            return value

    def __getitem__(self, key):
        value = self._obj[key]
        if not isinstance(value, ObservedObject):
            value = ObservedObject(value, rootObject=self._root)
            self._obj[key] = value
        return value

    def __setitem__(self, key, value):
        self._obj[key] = value
        self._root.modified = True

    def __str__(self):
        return str(self._obj)

    def __repr__(self):
        return repr(self._obj)

init_data = {"dict": {"liste":[1,2,3], "test1": 1}, "test2": "jup"} 

d = ObservedObject(init_data) 
print d 
print "-"*79 

d["dict"]["liste"][1] = 9 
print d
print "modified? ", d.modified
d.modified = False
print 

d["dict"]["test1"] = "yes" 
print d
print "modified? ", d.modified
d.modified = False 
print 

d["test"] = [1,2] 
print d
print "modified? ", d.modified
d.modified = False 
print

e = {'foo': 'bar'}
d['narf'] = e
print d
print "modified? ", d.modified
d.modified = False
print

e['foo'] = 'hurz'
print d
print "modified? ", d.modified #falsch
d.modified = False
print