2015-05-14 9 views
11

Früher heute habe ich die Frage "Raise error if python dict comprehension overwrites a key" gelesen und beschlossen, meine Hand auf eine Antwort zu versuchen. Die Methode, die mir natürlich in den Sinn kam, war die Unterklasse dict dafür. Ich blieb jedoch bei meiner Antwort hängen, und jetzt bin ich besessen davon, das für mich selbst zu erarbeiten.Überschreiben dict.update() -Methode in der Unterklasse, um das Überschreiben von Diktatschlüsseln zu verhindern

Hinweise:

  • Nein - ich habe nicht als Antwort auf diese Frage in der Antwort auf Drehung in der anderen Frage.
  • Dies ist eine rein intellektuelle Übung für mich an dieser Stelle. Praktisch würde ich fast sicher ein oder ein normales Wörterbuch verwenden, wo immer ich eine Anforderung für so etwas habe.

Meine (nicht ganz funktioniert) Lösung:

class DuplicateKeyError(KeyError): 
    pass 



class UniqueKeyDict(dict): 
    def __init__(self, *args, **kwargs): 
     self.update(*args, **kwargs) 


    def __setitem__(self, key, value): 
     if key in self: # Validate key doesn't already exist. 
      raise DuplicateKeyError('Key \'{}\' already exists with value \'{}\'.'.format(key, self[key])) 
     super().__setitem__(key, value) 


    def update(self, *args, **kwargs): 
     if args: 
      if len(args) > 1: 
       raise TypeError('Update expected at most 1 arg. Got {}.'.format(len(args))) 
      else: 
       try: 
        for k, v in args[0]: 
         self.__setitem__(k, v) 
       except ValueError: 
        pass 

     for k in kwargs: 
      self.__setitem__(k, kwargs[k]) 

Meine Tests und die erwarteten Ergebnisse

>>> ukd = UniqueKeyDict((k, int(v)) for k, v in ('a1', 'b2', 'c3', 'd4')) # Should succeed. 
>>> ukd['e'] = 5 # Should succeed. 
>>> print(ukd) 
{'a': 1, 'b': 2, 'c': 3, d: 4, 'e': 5} 
>>> ukd['a'] = 5 # Should fail. 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "<stdin>", line 8, in __setitem__ 
__main__.DuplicateKeyError: Key 'a' already exists with value '1'. 
>>> ukd.update({'a': 5}) # Should fail. 
>>> ukd = UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'd4', 'a5')) # Should fail. 
>>> 

Ich bin sicher, dass das Problem in meinem update() Methode ist, aber ich bin nicht in der Lage zu bestimmen, was ich gerade falsch mache.

Unten ist die Originalversion meiner update() Methode. Diese Version schlägt wie erwartet bei Duplikaten beim Aufruf von my_dict.update({k: v}) für ein Schlüssel/Wert-Paar bereits im dict fehl, scheitert jedoch nicht, wenn ein Duplikatschlüssel beim Erstellen des ursprünglichen dict eingefügt wird, da die Konvertierung der Argumente in dict zu einem Fehler führt Verhalten für ein Wörterbuch, dh Überschreiben des doppelten Schlüssels.

def update(self, *args, **kwargs): 
    for k, v in dict(*args, **kwargs).items(): 
     self.__setitem__(k, v) 
+1

Wenn höchstens ein Positions Argument erlaubt ist, verwenden Sie 'Update (self, arg = None, ** kwargs)' statt Erlauben einer beliebigen Anzahl von Argumenten. – chepner

+0

O/t, sollte aber nicht 'DuplicateKeyError' Unterklasse' KeyError'? – jonrsharpe

+0

@jonrsharpe - Ich dachte vor kurzem darüber nach und werde das Update machen. –

Antwort

3

Beachten Sie, dass je der Dokumentation:

  • dict.update nimmt einen einzigen other Parameter, „entweder ein anderes Wörterbuch Objekt oder ein iterable von Schlüssel/Wert-Paare“ (Ich habe collections.Mapping verwendet für diesen Test) und "Wenn Schlüsselwortargumente angegeben sind, wird das Wörterbuch dann mit diesen Schlüssel/Wert-Paaren aktualisiert"; und
  • dict() dauert eine einzige Mapping oder Iterable zusammen mit optional **kwargs (das gleiche wie update akzeptiert ...).

Dies ist nicht ganz die Schnittstelle, die Sie implementiert haben, was zu einigen Problemen führt. Ich würde dies wie folgt implementiert:

from collections import Mapping 


class DuplicateKeyError(KeyError): 
    pass 


class UniqueKeyDict(dict): 

    def __init__(self, other=None, **kwargs): 
     super().__init__() 
     self.update(other, **kwargs) 

    def __setitem__(self, key, value): 
     if key in self: 
      msg = 'key {!r} already exists with value {!r}' 
      raise DuplicateKeyError(msg.format(key, self[key])) 
     super().__setitem__(key, value) 

    def update(self, other=None, **kwargs): 
     if other is not None: 
      for k, v in other.items() if isinstance(other, Mapping) else other: 
       self[k] = v 
     for k, v in kwargs.items(): 
      self[k] = v 

Im Einsatz:

>>> UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'd4')) 
{'c': '3', 'd': '4', 'a': '1', 'b': '2'} 
>>> UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'a4')) 
Traceback (most recent call last): 
    File "<pyshell#8>", line 1, in <module> 
    UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'a4')) 
    File "<pyshell#7>", line 5, in __init__ 
    self.update(other, **kwargs) 
    File "<pyshell#7>", line 15, in update 
    self[k] = v 
    File "<pyshell#7>", line 10, in __setitem__ 
    raise DuplicateKeyError(msg.format(key, self[key])) 
DuplicateKeyError: "key 'a' already exists with value '1'" 

und:

>>> ukd = UniqueKeyDict((k, v) for k, v in ('a1', 'b2', 'c3', 'd4')) 
>>> ukd.update((k, v) for k, v in ('e5', 'f6')) # single Iterable 
>>> ukd.update({'h': 8}, g='7') # single Mapping plus keyword args 
>>> ukd 
{'e': '5', 'f': '6', 'a': '1', 'd': '4', 'c': '3', 'h': 8, 'b': '2', 'g': '7'} 

Wenn Sie jemals diese am Ende mit, ich wäre geneigt, es zu geben, eine andere __repr__ um Verwechslungen zu vermeiden!

+0

Ich hatte noch nicht die Gelegenheit, Ihre Antwort (oder eine der anderen) zu testen, aber eine kurze Frage zu Ihrer Implementierung: Aus irgendeinem Grund haben Sie sich dafür entschieden, 'Iterable' anstelle von' abc.Iterable' zu ​​verwenden, was " War ich meine normale Wahl, wenn ich gedacht hätte, diesen Weg zu gehen? Auch der Punkt über den '__repr__' ist ein guter. Ich würde es immer noch nicht benutzen, aber ich habe mir ein paar Anwendungsfälle dafür ausgedacht. Nicht sicher, ob sie die zusätzliche Komplexität gewährleisten würden. –

+0

@DougR. vorausgesetzt, du meinst 'collections.abc.Iterable' (es gibt kein' Iterable' in ['abc'] (https://docs.python.org/3/library/abc.html)), ist es ein Alias ​​für die gleiche Sache : * "Geändert in Version 3.3: Gestreifte Basisklassen wurden in das Modul' collections.abc' verschoben. Aus Gründen der Abwärtskompatibilität sind sie auch weiterhin in diesem Modul sichtbar. "* – jonrsharpe

+0

** Seufz ** - Ich habe präziser sein. Du hast absolut recht - ich meinte "collections.abc.Iterable". Danke für die Info - ich hätte selbst nachsehen sollen. Ich werde auf jeden Fall heute Abend einen Blick darauf werfen und mit Code spielen. –

4

Ich bin nicht sicher, ob dies das Problem ist, aber ich habe gerade bemerkt, dass Sie Ihre args im update Methode als eine Liste von Paaren behandeln:

for k, v in args[0] 

während Sie tatsächlich ein Wörterbuch Versorgung:

ukd.update({'a': 5}) 

Haben Sie versucht:

try: 
    for k, v in args[0].iteritems(): 
     self.__setitem__(k, v) 
except ValueError: 
    pass 

EDIT: Wahrscheinlich ist dieser Fehler unbemerkt geblieben, weil Sie except ing ValueError sind, was die Behandlung eines Wörterbuchs als eine Liste von Paaren erhöht.

+0

Sie haben absolut Recht über den Fehler, den ich machte, aber das endete nicht das eigentliche Problem. Danke für den Fang! –

4

Es ist interessant, dass einfach überschreiben __setitem__ ist nicht genug, um das Verhalten von update in dict zu ändern. Ich hätte erwartet, dass dict seine __setitem__ Methode verwenden würde, wenn es mit update aktualisiert wird. In allen Fällen, ich denke, es ist besser collections.MutableMapping zu implementieren, ohne zu berühren update das gewünschte Ergebnis zu erzielen:

import collections 

class UniqueKeyDict(collections.MutableMapping, dict): 

    def __init__(self, *args, **kwargs): 
     self._dict = dict(*args, **kwargs) 

    def __getitem__(self, key): 
     return self._dict[key] 

    def __setitem__(self, key, value): 
     if key in self: 
      raise DuplicateKeyError("Key '{}' already exists with value '{}'.".format(key, self[key])) 
     self._dict[key] = value 

    def __delitem__(self, key): 
     del self._dict[key] 

    def __iter__(self): 
     return iter(self._dict) 

    def __len__(self): 
     return len(self._dict) 

Edit: inbegriffen dict als Basisklasse der isinstance(x, dict) Prüfung zu erfüllen.

+1

Gute Verwendung der mitgelieferten ABCs, aber auf diese Weise müssen Sie sechs Methoden anstelle von drei implementieren und (falls es wichtig ist) 'isinstance (UniqueKeyDict(), dict)' wird 'False' sein (obwohl natürlich Leute verwenden sollten 'isinstance (..., collections.Mapping)'!) – jonrsharpe

+0

Das war für mich auch nicht einleuchtend, als ich meine erste Iteration von 'UniqueKeyDict' schrieb (nicht der beste Name, aber der beste, den ich kurzfristig finden konnte). Der Grund dafür, dass ich "dict" abschließe, ist, dass 'isinstance (UniqueKeyDict(), dict)' 'True' ist, genauso wie die Tatsache, dass ich nicht alle verschiedenen Methoden neu implementieren muss. –

+0

Nun, wir können immer dict in die Basisklassen aufnehmen, um die 'isinstance'-Prüfung zu erfüllen, ohne das Verhalten der Klasse zu beeinflussen – sirfz

2

Ich konnte das Ziel mit dem folgenden Code erreichen:

class UniqueKeyDict(dict): 
    def __init__(self, *args, **kwargs): 
     self.update(*args, **kwargs) 

    def __setitem__(self, key, value): 
     if self.has_key(key): 
      raise DuplicateKeyError("%s is already in dict" % key) 
     dict.__setitem__(self, key, value) 

    def update(self, *args, **kwargs): 
     for d in list(args) + [kwargs]: 
      for k,v in d.iteritems(): 
       self[k]=v 
+0

Hmm. 'ukd.update ({'a', 5})' ist jetzt der einzige Test, der fehlschlägt. Ich erhalte den Fehler "ValueError: brauche mehr als 1 Wert zum Entpacken". Es sollte fehlschlagen, aber mit einem 'DuplicateKeyError'. Ich habe auch 'ukd.update ({'f': 6})' versucht, was ** nicht ** einen Fehler erzeugt haben sollte, aber den gleichen Fehler bekommen hat. –

+0

Welche Python-Version verwenden Sie? Ich habe folgendes benutzt und es hat gut funktioniert: 'i = UniqueKeyDict ({1: 1, 2: 2, 3: 3}) i.update ({'a': 1}) i.update ({' a ': 2}) '' – PEdroArthur

+0

Python 3. Ich nehme von 'd.iteritems()' an, dass Sie Python 2 benutzen? Als ich Ihre Herangehensweise in 3 änderte, bestand der einzige Unterschied darin, dass ich das 'for' in zwei Teile aufteilte, einen für die Verarbeitung von' args' und einen für die Verarbeitung von 'kwargs'. Wenn ich es wie in Python3 benutze, indem ich einfach 'd.iteritems()' in 'd.items()' ändere, dann wird alles zum Boom. –

1

Warum mit setdefault nicht von MultiKeyDict inspiriert etwas entlang der Linien? Dadurch bleibt die Aktualisierungsmethode eine Möglichkeit, die aktuell gespeicherten Werte zu überschreiben, was, wie ich weiß, die Absicht d [k] = v == d.update ({k, v}) bricht. In meiner Anwendung war die Überschreibung nützlich. Bevor Sie also diese Frage nicht beantworten, denken Sie bitte daran, dass diese Antwort für jemand anderen nützlich sein könnte.

class DuplicateKeyError(KeyError): 
    """File exception rasised by UniqueKeyDict""" 
    def __init__(self, key, value): 
     msg = 'key {!r} already exists with value {!r}'.format(key, value) 
     super(DuplicateKeyError, self).__init__(msg) 


class UniqueKeyDict(dict): 
    """Subclass of dict that raises a DuplicateKeyError exception""" 
    def __setitem__(self, key, value): 
     if key in self: 
      raise DuplicateKeyError(key, self[key]) 
     self.setdefault(key, value) 


class MultiKeyDict(dict): 
    """Subclass of dict that supports multiple values per key""" 
    def __setitem__(self, key, value): 
     self.setdefault(key, []).append(value) 

eher neu zu Python so Flamme auf, verdienen es wahrscheinlich ...

Verwandte Themen