2.10. Sets

Een set is een ongeordende verzameling van unieke items. Een waarde toevoegen die al aanwezig is heeft geen effect; iteratie levert elke waarde precies één keer op. Sets zijn het juiste hulpmiddel wanneer lidmaatschap en ontdubbeling belangrijk zijn en volgorde niet.

2.10.1. Een set maken

Gebruik accolades voor een niet-lege set, of set() voor een lege:

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

De accolades zien eruit als een dict-literal; {} op zichzelf is een lege dict, geen lege set – een van Pythons historische toevalligheden. Gebruik set() voor het lege geval.

set() bouwt ook een set uit elke iterabele, wat de standaardmanier is om duplicaten uit een reeks te verwijderen:

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

Uitvoer:

{1, 2, 3, 4}

De printvolgorde kan variëren – sets garanderen geen iteratie in een bepaalde volgorde.

2.10.2. Set versus dict

Sets en dicts slaan beide unieke items op in een hashtabel. Wat elk item met zich meedraagt is het verschil:

  • Een dict slaat sleutel-waardeparen op. Het opzoeken van een sleutel retourneert de bijbehorende waarde.

  • Een set slaat alleen de items op. Het opzoeken van een item vertelt je of het aanwezig is.

De keuze tussen de twee gaat over de vraag of de waarde naast elk item iets betekent:

  • Grijp naar een set wanneer er geen waarde naast elk item hoort – je geeft er alleen om of het item aanwezig is, of je combineert groepen unieke items met vereniging / doorsnede.

  • Grijp naar een dict wanneer elk item gekoppeld is aan gegevens die de opzoeking moet ophalen – een configuratiekaart, een cache, een teller geïndexeerd op naam.

De twee typen delen veel oppervlakkige syntaxis, en daar komt de meeste verwarring vandaan. De verschillen in één blok:

set

dict

bevat

unieke items

unieke sleutels, elk met een waarde

gevulde literal

{1, 2, 3}

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

lege literal

set()

{}

lidmaatschapstest

x in s

k in d (alleen sleutels)

een waarde ophalen

n.v.t.

d[k]

een item toevoegen

s.add(x)

d[k] = v

itereren

levert items op

levert sleutels op (gebruik d.items() voor paren)

De asymmetrie tussen de gevulde en de lege literals is de valkuil die het waard is om te benoemen:

  • Accolades met items erin{1, 2, 3} – vormen een set-literal; accolades met sleutel-waardeparen{"a": 1} – vormen een dict-literal. De parser onderscheidt ze op basis van wat erbinnen staat.

  • Accolades met niets erbinnen{} – vormen een lege dict, geen lege set. Dicts kwamen eerst; de lege literal hoort bij hen. Een lege set heeft helemaal geen accoladeliteral en moet worden geschreven als set().

Een veelvoorkomend patroon, wanneer alleen de sleutels van een dict ooit worden gelezen, is om over te schakelen naar een set – dat maakt de bedoeling duidelijk en snijdt de ongebruikte waarden uit het geheugen weg.

2.10.3. Toevoegen en verwijderen

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

Uitvoer:

{1, 3, 4}

2.10.4. Lidmaatschap

De in-operator test op lidmaatschap. Bij een set verloopt dit ongeveer in constante tijd, ongeacht de grootte – wat de belangrijkste reden is om een set te kiezen boven een list wanneer je alleen hoeft te vragen “zit deze waarde erin”:

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

Een list met dezelfde inhoud zou elke keer vanaf het begin scannen, wat prima is voor tien items maar traag voor tienduizend.

2.10.5. Setbewerkingen

Twee sets kunnen worden gecombineerd met de gebruikelijke wiskundige bewerkingen. Elke bewerking heeft zowel een operatorvorm als een methodevorm:

  • a | b of a.union(b) – alles in een van beide sets.

  • a & b of a.intersection(b) – alleen wat in beide voorkomt.

  • a - b of a.difference(b) – wat in a zit maar niet in b.

  • a ^ b of a.symmetric_difference(b) – wat in een van beide zit maar niet in beide.

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

Uitvoer:

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

De operatorvormen zijn alleen-lezen; de methodevormen accepteren elke iterabele aan de rechterkant, niet alleen een andere set (a.union([5, 6])). Kies wat in de context het beste leest.

2.10.6. Wat er in een set kan

Set-elementen moeten hashbaar zijn – dezelfde beperking als bij dict-sleutels. int, float, str, bool, bytes en tuple (wanneer de inhoud zelf hashbaar is) werken allemaal. list en dict niet; er een proberen toe te voegen veroorzaakt een TypeError.

2.10.7. frozenset

Een gewone set is veranderlijk: elke aanroep van add / remove / discard wijzigt het object ter plekke. Die veranderlijkheid maakt dat het niet hashbaar kan zijn, dus een set kan niet worden gebruikt als dict-sleutel of als lid van een andere set.

frozenset is de onveranderlijke tegenhanger. Het heeft dezelfde opzoekingen en operatoren (in, |, &, -, ^) als set, maar geen add / remove en geen methoden die muteren. Omdat de inhoud nooit kan veranderen, is de hash van een frozenset welgedefinieerd – dus het is hashbaar:

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

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

print(palettes[primary])

Uitvoer:

RGB

Construeer een frozenset uit elke iterabele – frozenset() voor het lege geval, frozenset(some_set) om een onveranderlijke momentopname van een bestaande set te nemen:

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

Twee veelvoorkomende redenen om ernaar te grijpen:

  • Gebruik als dict-sleutel of set-lid. Overal waar een enkele waarde niet kan vatten wat je nodig hebt, kan een frozenset van waarden dat wel – “de set kenmerken die deze driver ondersteunt”, “de set pinnen die dit profiel gebruikt”.

  • Een constante vastleggen. Een frozenset op moduleniveau van toegestane namen kan niet per ongeluk door een aanroeper worden gemuteerd; een gewone set wel. Geef de voorkeur aan frozenset voor alles wat na constructie alleen-lezen hoort te zijn.