2.10. Sets

Ein Set ist eine ungeordnete Sammlung eindeutiger Elemente. Das Hinzufügen eines bereits vorhandenen Werts hat keine Wirkung; die Iteration liefert jeden Wert genau einmal. Sets sind das richtige Werkzeug, wenn Mitgliedschaft und Entduplizierung wichtig sind und die Reihenfolge keine Rolle spielt.

2.10.1. Ein Set erstellen

Verwenden Sie geschweifte Klammern für ein nicht leeres Set oder set() für ein leeres:

colours = {"red", "green", "blue"}
empty = set()

Die Klammern sehen aus wie ein dict-Literal; {} allein ist ein leeres Dict, kein leeres Set – einer von Pythons historischen Unfällen. Verwenden Sie set() für den leeren Fall.

set() erstellt auch ein Set aus jedem Iterable, was die übliche Methode ist, um Duplikate aus einer Folge zu entfernen:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

Ausgabe:

{1, 2, 3, 4}

Die Druckreihenfolge kann variieren – Sets versprechen nicht, in einer bestimmten Reihenfolge zu iterieren.

2.10.2. Set vs. Dict

Sets und Dicts speichern beide eindeutige Elemente in einer Hashtabelle. Der Unterschied liegt darin, was jedes Element mit sich trägt:

  • Ein dict speichert Schlüssel-Wert-Paare. Das Nachschlagen eines Schlüssels gibt seinen Wert zurück.

  • Ein set speichert nur die Elemente. Das Nachschlagen eines Elements sagt Ihnen, ob es vorhanden ist.

Die Wahl zwischen beiden hängt davon ab, ob der Wert neben jedem Element irgendeine Bedeutung hat:

  • Greifen Sie zu einem Set, wenn kein Wert zu jedem Element gehört – Sie interessieren sich nur dafür, ob das Element vorhanden ist, oder Sie kombinieren Gruppen eindeutiger Elemente mit Vereinigung / Schnittmenge.

  • Greifen Sie zu einem Dict, wenn jedes Element mit Daten gepaart ist, die das Nachschlagen abrufen soll – eine Konfigurationszuordnung, ein Cache, ein nach Namen indizierter Zähler.

Die beiden Typen teilen sich viel an Oberflächensyntax, woher die meiste Verwirrung kommt. Die Unterschiede in einem Block:

Set

Dict

enthält

eindeutige Elemente

eindeutige Schlüssel, jeder mit einem Wert

befülltes Literal

{1, 2, 3}

{"a": 1, "b": 2}

leeres Literal

set()

{}

Mitgliedschaftstest

x in s

k in d (nur Schlüssel)

einen Wert abrufen

n/a

d[k]

ein Element hinzufügen

s.add(x)

d[k] = v

iterieren

liefert Elemente

liefert Schlüssel (verwenden Sie d.items() für Paare)

Die Asymmetrie zwischen den befüllten und leeren Literalen ist die Fallstrick, der erwähnenswert ist:

  • Klammern mit Elementen darin{1, 2, 3} – sind ein Set-Literal; Klammern mit Schlüssel-Wert-Paaren{"a": 1} – sind ein Dict-Literal. Der Parser unterscheidet sie anhand dessen, was darin steht.

  • Klammern mit nichts darin{} – sind ein leeres Dict, kein leeres Set. Dicts kamen zuerst; das leere Literal gehört ihnen. Ein leeres Set hat überhaupt kein Klammernliteral und muss als set() geschrieben werden.

Ein gängiges Muster, wenn nur die Schlüssel eines Dicts jemals gelesen werden, besteht darin, zu einem Set zu wechseln – das macht die Absicht offensichtlich und entfernt die ungenutzten Werte aus dem Speicher.

2.10.3. Hinzufügen und Entfernen

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

Ausgabe:

{1, 3, 4}

2.10.4. Mitgliedschaft

Der in-Operator prüft die Mitgliedschaft. Bei einem Set ist er unabhängig von der Größe annähernd konstant in der Zeit – was der Hauptgrund ist, ein Set einer list vorzuziehen, wenn Sie nur fragen müssen „ist dieser Wert darin“:

if "red" in colours:
    print("colour is allowed")

Eine list mit demselben Inhalt würde jedes Mal von vorne durchsucht, was für zehn Elemente in Ordnung ist, aber für zehntausend langsam ist.

2.10.5. Set-Operationen

Zwei Sets können mit den üblichen mathematischen Operationen kombiniert werden. Jede hat sowohl eine Operatorform als auch eine Methodenform:

  • a | b oder a.union(b) – alles in einem der beiden Sets.

  • a & b oder a.intersection(b) – nur das, was in beiden vorkommt.

  • a - b oder a.difference(b) – in a, aber nicht in b.

  • a ^ b oder a.symmetric_difference(b) – in einem, aber nicht in beiden.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

Ausgabe:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

Die Operatorformen sind schreibgeschützt; die Methodenformen akzeptieren auf der rechten Seite jedes Iterable, nicht nur ein anderes Set (a.union([5, 6])). Wählen Sie, was sich im Kontext besser liest.

2.10.6. Was in ein Set kommen kann

Set-Elemente müssen hashbar sein – dieselbe Einschränkung wie bei dict-Schlüsseln. int, float, str, bool, bytes und tuple (wenn dessen Inhalt selbst hashbar ist) funktionieren alle. list und dict nicht; der Versuch, eines hinzuzufügen, löst TypeError aus.

2.10.7. frozenset

Ein reguläres set ist veränderlich: jeder Aufruf von add / remove / discard ändert das Objekt an Ort und Stelle. Diese Veränderlichkeit schließt es davon aus, hashbar zu sein, sodass ein Set nicht als dict-Schlüssel oder als Element eines anderen Sets verwendet werden kann.

frozenset ist das unveränderliche Gegenstück. Es hat dieselben Nachschlagevorgänge und Operatoren (in, |, &, -, ^) wie set, aber kein add / remove und keine Methoden, die verändern. Da sich sein Inhalt niemals ändern kann, ist der Hash eines frozenset wohldefiniert – es ist also hashbar:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

Ausgabe:

RGB

Erstellen Sie ein frozenset aus jedem Iterable – frozenset() für den leeren Fall, frozenset(some_set), um einen unveränderlichen Schnappschuss eines bestehenden Sets zu erstellen:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

Zwei häufige Gründe, dazu zu greifen:

  • Verwendung als Dict-Schlüssel oder Set-Element. Überall dort, wo ein einzelner Wert nicht erfassen kann, was Sie brauchen, kann ein frozenset von Werten dies tun – „die Menge der von diesem Treiber unterstützten Merkmale“, „die Menge der von diesem Profil verwendeten Pins“.

  • Eine Konstante absichern. Ein frozenset zulässiger Namen auf Modulebene kann nicht versehentlich von einem Aufrufer verändert werden; ein reguläres set schon. Bevorzugen Sie frozenset für alles, was nach der Konstruktion schreibgeschützt sein soll.