Werken met sets

Inleiding

In voorgaande hoofdstukken leerde je over de lists en de tuples. In dit hoofdstuk leer je over de set. Ook de set is een collectie, maar heeft een heel andere functie dan de list en de tuple.

Het belangrijkste kenmerk van een set is dat het enkel unieke elementen bevat. Verder gebruik je sets met name om twee groepen elementen met elkaar te vergelijken (welk element is onderdeel van beide groepen, bijvoorbeeld).

Leerdoelen

Aan het einde van dit hoofdstuk:

  • Begrijp je wat een set is

  • Begrijp je in welke situatie je sets toe kunt passen

  • Begrijp je hoe je met elementen in een set kunt werken

Sets: een inleiding

In veel opzichten lijkt ook een set op een list. Het is een type collectie dat verschillende elementen kan bevatten, waar je langs kunt itereren en elementen aan kunt toevoegen of uit kunt halen.

Er zijn echter ook grote verschillen. In deze paragraaf leer je over de unieke eigenschappen van een set en wanneer je een set kunt gebruiken.

Kenmerken van sets

Een set maak je op verschillende manieren:

lege_set = set()
set_met_items = {"a", "b", "c", }

mijn_list = [1, 2, 3, ]
mijn_set = set(mijn_list)

Een lege set maak je altijd met de set()-constructor. Omdat je een set met elementen aanmaakt met accolades, verwacht je misschien dat je een lege set ook met accolades aan kunt maken ({}), maar dat is al gereserveerd om een lege dict aan te maken!

Een set bestaat altijd uit één of meer elementen tussen accolades, gescheiden door een komma.

De belangrijkste kenmerken van een set zijn:

  • Een set is een type collectie.

  • Een set zelf is muteerbaar, maar de elementen in een set mogen dat niet zijn.

  • Elementen in een set mogen van verschillende typen zijn (als ze maar niet muteerbaar zijn).

  • Elementen in een set zijn altijd uniek.

  • De volgorde van elementen in een set staat niet vast.

Hashable

Net als de bij de sleutels van een dict, gaat het er bij een set om dat de elementen hashable zijn. De meeste ingebouwde types in Python die muteerbaar zijn, zijn niet hashable. In de praktijk is het echter wel mogelijk om een datatype te hebben die veranderlijk is, maar toch hashable. Voor nu is het voldoende om er vanuit te gaan dat een element van een set onveranderlijk moet zijn.

Op Wikipedia lees je wat een hash is: nl.wikipedia.org/wiki/Hashfunctie

Omdat een set een collectie is, kun je er langs itereren. Verder zijn andere acties van collecties ook mogelijk:

  • Controleren of het element i in de set aanwezig is met i in mijn_set.

  • Het aantal elementen ophalen met len(mijn_set).

  • Het kleinste en grootste element ophalen met min(mijn_set) en max(mijn_set).

Indexering werkt niet, door de ongeordende structuur (de volgorde van elementen staat niet vast).

Geordend en ongeordend

Hoe zit dat met geordende en ongeordende datatypen? Een set is ongeordend. De term doet vermoeden dat het iets met een gesorteerde volgorde te maken heeft, maar niets is minder waar. Dat een set ongeordend is, wil zeggen dat de volgorde van de elementen niet vast staat.

Stel je maakt een set aan met drie elementen, in de volgende volgorde: {2, 3, 1}.

Als de elementen nu in een for-loop gaat printen, kan het best zijn dat eerst de 3 wordt geprint, dan de 1 en dan de 2.

Kortom: de volgorde van aanmaken zegt niets over de volgorde van opslaan. Vandaar dat indexering niet werkt. En zoals je verderop ziet, heeft ook sorteren en omkeren door dit gegeven geen zin.

Daarnaast zijn er nog een aantal acties die speciaal aan sets zijn voorbehouden, hierover lees je in de volgende paragraaf.

Wanneer gebruik je sets

Een set gebruik je vrijwel altijd om twee redenen:

  • Je wilt enkel met unieke waarden werken.

  • Je wilt verschillende groepen elementen met elkaar vergelijken.

Om een voorbeeld bij dit laatste te geven, stel dat je twee sets met personen hebt, en je wilt weten welke persoon in beide sets aanwezig is:

set_a = {"Marie", "John", "Erwin", }
set_b = {"Ali", "John", }

in_beide = set_a.intersection(set_b)
print(in_beide)  # {"John"}

In de volgende paragraaf leer je meer van dergelijke berekeningen te maken.

Werken met elementen in een set

Een set wijkt in meer opzichten af van een list dan een tuple doet. In deze paragraaf leer je hoe te werken met elementen in een set, waarbij weer de nadruk ligt op handelingen die anders werken dan bij een list.

Elementen uit een set ophalen

De set is niet erg geschikt om met individuele elementen te werken. Een element uit een set verkrijgen is dan ook niet eenvoudig. Indexering werkt bijvoorbeeld niet, door de ongeordende structuur. Je kunt .pop() gebruiken om een willekeurig element uit de set te verkrijgen (en te verwijderen).

Wil je met één element uit een set werken, dan kun je het beste langs alle elementen itereren, tot je het gewenste element bereikt hebt.

Sorteren en omkeren

Net als bij tuples heeft een set ook geen .sort() en .reverse() methodes. In dit geval niet omdat een set niet aanpasbaar is (want dat is het wel), maar omdat een set ongeordend is (zie kader eerder). Je kunt een set wel sorteren (met sorted), maar het zal de volgorde niet behouden. Wil je dit toch, dan dien je er (bijvoorbeeld) een list of een tuple van te maken.

Elementen aan een set toevoegen

Om een enkel element aan een set toe te voegen gebruik je de .add() methode. Om meerdere elementen uit een iterable toe te voegen, gebruik je .update() (net als bij een list).

mijn_set = {1, 2, 3, }
mijn_set.add(4)

print(mijn_set)
# {1, 2, 3, 4}

mijn_list = [5, 6, 7, ]
mijn_set.update(mijn_list)

print(mijn_set)
# {1, 2, 3, 4, 5, 6, 7}

Let op dat in beide gevallen geen foutmelding wordt gegeven als je een element toevoegt dat al in de set aanwezig is.

Elementen uit een set verwijderen

Om een element uit een set te verwijderen, gebruik je .remove() of .discard(). In beide gevallen geef je de waarde van het element op dat je wilt verwijderen. Het verschil tussen beiden is dat als het betreffende element niet bestaat, .remove() een foutmelding geeft en .discard() niet.

mijn_set = {1, 2, 3, 4, }
mijn_set.remove(1)

print(mijn_set)
# {2, 3, 4}

mijn_set.remove(5)  # Fout
mijn_set.discard(5)  # Geen fout
mijn_set.discard(2)

print(mijn_set)
# {3, 4}

Welke methode je gebruikt hangt af van de context. In veel gevallen wil je weten dat je een element probeert te verwijderen die er niet is en wil je dat afhandelen (hoe je dat doet, leer je in het hoofdstuk Werken met uitzonderingen). In die gevallen gebruik je .remove(). Het kan echter zijn dat het geen probleem is dat je programma zonder melding doorloopt als je een element wilt verwijderen dat niet bestaat. In dat geval kun je .discard() gebruiken.

Tot slot kun je - net als bij een list of dict - de methode .pop() gebruiken. De werking is wel anders.

  • Bij een list verwijdert .pop() het laatste element. Je kunt het ook een index meegeven om het element op die index te verwijderen.

  • Bij een dict dien je altijd een sleutel mee te geven aan .pop(). De waarde behorende bij die sleutel zal verwijderd worden.

  • Bij een set kun je geen waarde aan .pop() meegeven. Het verwijdert een willekeurig element uit de set.

In alle gevallen wordt het verwijderde element ook teruggegeven.

Opdracht 1: .pop()

Leg in je eigen woorden uit waarom .pop() niet het laatste element van een set geeft zoals bij een list.

Klik om het antwoord te tonen

Een set is een ongeordende datastructuur. Per definitie is er daarom niet zoiets als een laatste (of eerste) element. .pop() kan daarom ook niet een laatste element verwijderen en teruggeven, omdat het concept laatste in een set niet van toepassing is.

Set vergelijkingen

Eén van de interessantere mogelijkheden van sets is het vergelijken van de elementen in verschillende sets. Uitgaande van set_1 en set_2, kun je snel nagaan:

  • Welke elementen in set_1 OF in set_2 (of beide) aanwezig zijn met .union.

  • Welke elementen in set_1 EN in set_2 aanwezig zijn met .intersection.

  • Welke elementen in set_1 maar NIET in set_2 aanwezig zijn met .difference.

  • Welke elementen in set_1 OF in set_2, maar NIET in beide aanwezig zijn met .symmetric_difference.

Om de set-vergelijkingen te illustreren maak je de volgende drie sets aan:

weekdagen = {
    "maandag",
    "dinsdag",
    "woensdag",
    "donderdag",
    "vrijdag",
    "zaterdag",
    "zondag"
}
lesdagen = {"maandag", "woensdag", "vrijdag"}
sportdagen = {"woensdag", "zondag"}

Allereerst zie je een set met alle dagen van de week. De andere twee sets zijn de dagen waarop je les hebt en waarop je sport.

Venn

Als je met sets werkt kan het handig zijn om één en ander te visualiseren. De meest gebruikte manier hiervoor is om met Venn-diagrammen te werken. Elke set wordt hierbij vertegenwoordigd door een cirkel, de cirkels kunnen elkaar deels overlappen. Bijvoorbeeld:

intersection

In elke cirkel noteer je de elementen, de elementen die in beide sets voorkomen noteer je in het overlappende deel.

Er zijn ook online tools die dit automatisch voor je doen, zie bijvoorbeeld: goodcalculators.com/venn-diagram-maker/

Union

Om nu na te gaan op welke dagen je iets hebt (sport, les, of beide) gebruik je .union:

# Union: dagen met les, sport of beide
bezette_dagen = lesdagen.union(sportdagen)
print(bezette_dagen)
# {'maandag', 'vrijdag', 'zondag', 'woensdag'}
union
Opdracht 2: Woensdag

Leg in je eigen woorden uit hoe het kan dat woensdag maar eenmaal in het resultaat voorkomt, terwijl je op woensdag sport én lessen volgt.

Klik om het antwoord te tonen

Een set kan geen dubbele waarden bevatten. Dus alhoewel woensdag tweemaal voorkomt, wordt deze waarde ontdubbelt.

Intersection

Om na te gaan wanneer je zowel sport als les hebt, gebruik je .intersection:

# Intersection: dagen met les én sport
drukke_dagen = lesdagen.intersection(sportdagen)
print(drukke_dagen)
# {'woensdag'}
intersection
Opdracht 3: Omdraaien

Wat is het resultaat van drukke_dagen = sportdagen.intersection(lesdagen) (lesdagen en sportdagen zijn omgedraaid)? Leg uit hoe dat kan.

Klik om het antwoord te tonen

Het resultaat is hetzelfde, namelijk {'woensdag'}. Omdat je met .intersection controleert welke elementen in zowel sportdagen als lesdagen voorkomen, maakt de volgorde niet uit. Dit werkt overigens net zo bij .union, en symmetric_difference. Een dergelijke bewerking - dat de volgorde niet uitmaakt - noem je commutatief.

Difference

Om na te gaan op welke dagen je vrij hebt (dus geen sport en geen les), ga je het verschil na tussen de weekdagen en de bezette_dagen:

# Difference: dagen in weekdagen, maar niet in bezette dagen
vrije_dagen = weekdagen.difference(bezette_dagen)
print(vrije_dagen)
# {'dinsdag', 'donderdag', 'zaterdag'}
difference

Je bekijkt zo de weekdagen die niet in bezette_dagen aanwezig zijn.

Als je het omdraait, dan bekijk je de bezette dagen die niet in weekdagen aanwezig zijn. Dat levert een lege set op, omdat alle dagen in bezette_dagen ook in weekdagen aanwezig is.

Symmetric Difference

Tot slot kun je nog nagaan op welke dag je maar één iets hebt (of les, of sport, maar niet beide):

# Symmetric Difference: dagen in lesdagen of sportdagen, maar niet in beide
een_ding = lesdagen.symmetric_difference(sportdagen)
print(een_ding)
# {'maandag', 'zondag', 'vrijdag'}
symmetric difference
Opdracht 4: Alternatief

Hoe kun je met een set-vergelijking nog meer nagaan op welke dagen je maar één activiteit hebt?

Klik om het antwoord te tonen

Dit kan als volgt: een_activiteit = bezette_dagen.difference(drukke_dagen). Op bezette_dagen heb je één of twee activiteiten, op drukke_dagen heb je twee activiteiten. De dagen die in bezette_dagen voorkomen, maar niet in drukke_dagen, zijn dus de dagen met één activiteit.

Subset, superset, disjoint

Daarnaast zijn er methodes om na te gaan of set_1:

  • Een subset is van set_2 (alle elementen van set_1 zijn ook aanwezig in set_2).

  • Een superset is van set_2 (alle elementen van set_2 zijn ook aanwezig in set_1).

  • Er geen enkele overlap is (geen enkel element van set_1 bevindt zich in set_2).

Neem de volgende, iets aangepaste sets:

weekdagen = {
    "maandag",
    "dinsdag",
    "woensdag",
    "donderdag",
    "vrijdag",
}
weekend = {"zaterdag", "zondag",}
lesdagen = {"maandag", "woensdag", "vrijdag"}
sportdagen = {"woensdag", "zondag"}

Je controleert eenvoudig of alle lesdagen onderdeel zijn van weekdagen met .issubset. Hetzelfde doe je voor de sportdagen:

lessen_in_week = lesdagen.issubset(weekdagen)
print(lessen_in_week)  # True

sport_in_week = sportdagen.issubset(weekdagen)
print(sport_in_week)  # False

Andersom kun je controleren of alle dagen van de week de lesdagen omvatten:

week_lessen = weekdagen.issuperset(lesdagen)
print(week_lessen)  # True

En tot slot kun je nagaan of twee sets totaal geen overlap hebben, zoals doordeweeks en het weekend:

geen_overlap = weekdagen.isdisjoint(weekend)
print(geen_overlap)  # True
Operatoren

In plaats van bijvoorbeeld set_a.union(set_b) kun je ook operatoren gebruiken. Voor union gebruik je |: set_a | set_b.

  • union: |

  • intersection: &

  • difference: -

  • symmetric_difference: ^

Zie docs.python.org/3.11/library/stdtypes.html#set voor alle operatoren en hun gebruik.