Python: Wie führt man zwei Dictionaries zusammen?
Zwei Dictionaries zusammenzuführen hat früher zwei Anweisungen gebraucht. Seit Python 3.9 braucht es ein Zeichen. Hier die Möglichkeiten — und die eine Eigenschaft, die sie alle teilen und die im Alltag Ärger macht.
Die kurze Antwort
zusammen = a | b
a = {'host': 'localhost', 'port': 3306}
b = {'port': 5432, 'user': 'admin'}
a | b : {'host': 'localhost', 'port': 5432, 'user': 'admin'}
a is unchanged : {'host': 'localhost', 'port': 3306}
Den |-Operator für Dictionaries gibt es seit Python 3.9, eingeführt mit
PEP 584. Unter Python 3.5 bis 3.8 nimmst
du stattdessen das Entpacken aus
PEP 448:
zusammen = {**a, **b}
{**a, **b} : {'host': 'localhost', 'port': 5432, 'user': 'admin'}
Beide bauen ein neues Dictionary und lassen die Originale in Ruhe.
Die rechte Seite gewinnt
a | b -> port = 5432 (b's value)
b | a -> port = 3306 (a's value)
Das ist genau das, was man für den häufigsten Fall braucht: Voreinstellungen links, Überschreibungen rechts.
konfig = VORGABEN | benutzer_konfig
Etwas feiner als die Werteregel ist die Reihenfolge. Ein Schlüssel, der in beiden vorkommt, behält die Position seines ersten Auftretens, übernimmt aber den Wert des letzten:
x = {'one': 1, 'two': 2}
y = {'two': 22, 'zero': 0}
x | y = {'one': 1, 'two': 22, 'zero': 0}
two ist an zweiter Stelle geblieben und hat die 22 übernommen.
An Ort und Stelle zusammenführen
Wenn du das linke Dictionary verändern willst, statt ein neues zu bauen:
a |= b # seit 3.9
a.update(b) # in jeder Version
Beides tut dasselbe. Womit wir beim Fehler wären, den jeder einmal macht:
update() returns : None
dict.update()
liefert None zurück, wie die meisten Methoden die an Ort und Stelle
verändern. Also führt
zusammen = a.update(b) # zusammen ist None
nichts in zusammen zusammen. Es verändert a und gibt dir None. Mit etwas
Glück wirft die nächste Zeile einen AttributeError auf NoneType, mit etwas
Pech reist das None noch eine Weile mit.
Drei und mehr
| lässt sich verketten, das Entpacken auch:
e | f | g : {'a': 1, 'b': 2, 'c': 3}
{**e, **f, **g} : {'a': 1, 'b': 2, 'c': 3}
Liegen die Dictionaries in einer Liste, nimmst du eine Schleife:
zusammen = {}
for eines in dictionaries:
zusammen |= eines
from a list, in a loop : {'a': 1, 'b': 2, 'c': 3}
Und jetzt der wunde Punkt: die Vereinigung ist flach
Alle oben gezeigten Formen kopieren genau eine Ebene tief. Sieh dir an, was mit einer verschachtelten Konfiguration passiert:
left = {'db': {'host': 'localhost', 'port': 3306}, 'debug': False}
right = {'db': {'port': 5432}}
left | right = {'db': {'port': 5432}, 'debug': False}
host ist weg. Den Schlüssel db gab es auf beiden Seiten, also hat der
gesamte Wert von rechts den gesamten Wert von links ersetzt. Niemand hat
hineingesehen.
Das ist kein Sonderfall. Verschachtelte Dictionaries sind genau das, wie Konfiguration aussieht, und “ich habe eine Einstellung überschrieben und den Rest des Abschnitts verloren” ist das Ergebnis.
Sollen die Abschnitte ebenfalls zusammengeführt werden, musst du das selbst schreiben:
def tief_zusammenfuehren(links: dict, rechts: dict) -> dict:
ergebnis = dict(links)
for schluessel, wert in rechts.items():
if isinstance(ergebnis.get(schluessel), dict) and isinstance(wert, dict):
ergebnis[schluessel] = tief_zusammenfuehren(ergebnis[schluessel], wert)
else:
ergebnis[schluessel] = wert
return ergebnis
deep_merge : {'db': {'host': 'localhost', 'port': 5432}, 'debug': False}
Flach heißt auch: die Hälften sind geteilt
Die zweite Hälfte desselben Problems, und die heimtückischere:
basis = {"cfg": {"n": 1}}
kopie = basis | {}
kopie["cfg"]["n"] = 99
after changing the copy, base = {'cfg': {'n': 99}}
same inner object? True
Das äußere Dictionary ist neu. Das innere ist dasselbe Objekt, auf das beide zeigen. Ein Schreibzugriff über die vermeintliche Kopie hat das Original verändert.
Nur copy.deepcopy
gibt dir eine unabhängige Struktur:
after copy.deepcopy and changing the copy, base = {'cfg': {'n': 1}}
same inner object? False
ChainMap, wenn du gar nicht zusammenführen willst
collections.ChainMap
durchsucht mehrere Dictionaries der Reihe nach, ohne etwas zu bauen:
ChainMap(b, a)['port'] : 5432 <- first mapping wins, so b
Zwei Dinge fallen auf. Die Rangfolge ist gegenüber | umgekehrt — hier
gewinnt das erste Mapping, nicht das letzte. Und weil nichts kopiert wurde,
sieht die ChainMap spätere Änderungen an den zugrundeliegenden Dictionaries:
after changing a, cm['host'] : changed-later
Das ist entweder genau das, was du wolltest, oder eine unangenehme Überraschung — je nachdem, ob du davon wusstest. Für geschichtete Konfiguration passt es gut: Kommandozeile über Umgebung über Datei über Vorgaben, ohne Kopieren und ohne Zweifel daran, woher ein Wert stammt.
Zwei Fehler, die dir begegnen können
{**a, **b} verträgt jeden hashbaren Schlüssel. dict(**a, **b), das du in
älteren Antworten findest, nicht:
n1 | n2 : {1: 'one', 2: 'two'}
{**n1, **n2} : {1: 'one', 2: 'two'}
dict(**n1, **n2) -> TypeError: keywords must be strings
Und | besteht auf zwei Dictionaries, während update() mit jedem
iterierbaren Objekt aus Paaren zufrieden ist:
dict | list -> TypeError: unsupported operand type(s) for |: 'dict' and 'list'
d.update(pairs) works : {'a': 1, 'b': 2}
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).