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, ...).