2.10. Zbiory

Zbiór to nieuporządkowana kolekcja unikalnych elementów. Dodanie wartości, która już występuje, nie ma żadnego efektu; iteracja zwraca każdą wartość dokładnie raz. Zbiory są właściwym narzędziem, gdy liczy się przynależność i usuwanie duplikatów, a kolejność nie ma znaczenia.

2.10.1. Tworzenie zbioru

Użyj nawiasów klamrowych dla niepustego zbioru lub set() dla pustego:

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

Nawiasy klamrowe wyglądają jak literał dict; samo {} to pusty słownik, a nie pusty zbiór – jeden z historycznych wypadków Pythona. Dla przypadku pustego użyj set().

set() buduje też zbiór z dowolnego obiektu iterowalnego, co jest standardowym sposobem usuwania duplikatów z sekwencji:

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

Wynik:

{1, 2, 3, 4}

Kolejność wypisywania może być różna – zbiory nie obiecują iterowania w żadnym określonym porządku.

2.10.2. Zbiór a słownik

Zarówno zbiory, jak i słowniki przechowują unikalne elementy w tablicy mieszającej. Różnica polega na tym, co każdy element ze sobą niesie:

  • dict przechowuje pary klucz-wartość. Wyszukanie klucza zwraca jego wartość.

  • set przechowuje tylko elementy. Wyszukanie elementu mówi, czy się on tam znajduje.

Wybór pomiędzy nimi zależy od tego, czy wartość towarzysząca każdemu elementowi cokolwiek oznacza:

  • Sięgnij po zbiór, gdy do każdego elementu nie należy żadna wartość – interesuje cię tylko, czy element jest obecny, lub łączysz grupy unikalnych elementów za pomocą sumy / przecięcia.

  • Sięgnij po słownik, gdy każdy element jest sparowany z danymi, które wyszukiwanie ma pobierać – mapa konfiguracji, pamięć podręczna, licznik kluczowany nazwą.

Oba typy dzielą wiele składni na powierzchni, co jest źródłem większości nieporozumień. Różnice w jednym bloku:

set

dict

przechowuje

unikalne elementy

unikalne klucze, każdy z wartością

literał wypełniony

{1, 2, 3}

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

literał pusty

set()

{}

test przynależności

x in s

k in d (tylko klucze)

pobranie wartości

nie dotyczy

d[k]

dodanie elementu

s.add(x)

d[k] = v

iterowanie

zwraca elementy

zwraca klucze (dla par użyj d.items())

Asymetria między literałem wypełnionym a pustym jest pułapką wartą podkreślenia:

  • Nawiasy klamrowe z elementami w środku{1, 2, 3} – to literał zbioru; nawiasy klamrowe z parami klucz-wartość{"a": 1} – to literał słownika. Parser rozróżnia je po tym, co znajduje się w środku.

  • Nawiasy klamrowe z niczym w środku{} – to pusty słownik, a nie pusty zbiór. Słowniki pojawiły się pierwsze; pusty literał należy do nich. Pusty zbiór nie ma żadnego literału z nawiasami klamrowymi i musi być zapisany jako set().

Częstym wzorcem, gdy odczytywane są tylko klucze słownika, jest przejście na zbiór – czyni to intencję oczywistą i usuwa nieużywane wartości z pamięci.

2.10.3. Dodawanie i usuwanie

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

Wynik:

{1, 3, 4}

2.10.4. Przynależność

Operator in sprawdza przynależność. Dla zbioru działa w przybliżeniu w stałym czasie niezależnie od rozmiaru – co jest głównym powodem, by wybrać zbiór zamiast list, gdy potrzebujesz tylko zapytać „czy ta wartość tam jest”:

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

list o tej samej zawartości za każdym razem skanowałaby od początku, co jest w porządku dla dziesięciu elementów, ale wolne dla dziesięciu tysięcy.

2.10.5. Operacje na zbiorach

Dwa zbiory można łączyć za pomocą zwykłych operacji matematycznych. Każda z nich ma zarówno postać operatorową, jak i metodową:

  • a | b lub a.union(b) – wszystko, co jest w którymkolwiek ze zbiorów.

  • a & b lub a.intersection(b) – tylko to, co pojawia się w obu.

  • a - b lub a.difference(b) – w a, ale nie w b.

  • a ^ b lub a.symmetric_difference(b) – w jednym, ale nie w obu.

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

Wynik:

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

Postacie operatorowe są tylko do odczytu; postacie metodowe przyjmują po prawej stronie dowolny obiekt iterowalny, a nie tylko inny zbiór (a.union([5, 6])). Wybierz tę, która lepiej czyta się w danym kontekście.

2.10.6. Co może trafić do zbioru

Elementy zbioru muszą być haszowalne – to samo ograniczenie, co dla kluczy dict. int, float, str, bool, bytes oraz tuple (gdy jej zawartość sama jest haszowalna) – wszystkie działają. list i dict nie; próba dodania jednego z nich zgłasza TypeError.

2.10.7. frozenset

Zwykły set jest zmienny: każde wywołanie add / remove / discard zmienia obiekt w miejscu. Ta zmienność dyskwalifikuje go z bycia haszowalnym, więc zbiór nie może być użyty jako klucz dict ani jako element innego zbioru.

frozenset to niezmienny odpowiednik. Ma te same wyszukiwania i operatory (in, |, &, -, ^) co set, ale nie ma add / remove ani żadnych metod, które go modyfikują. Ponieważ nic nigdy nie może zmienić jego zawartości, hasz frozenset jest dobrze określony – więc jest haszowalny:

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

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

print(palettes[primary])

Wynik:

RGB

Skonstruuj frozenset z dowolnego obiektu iterowalnego – frozenset() dla przypadku pustego, frozenset(some_set), aby wziąć niezmienny zrzut istniejącego zbioru:

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

Dwa typowe powody, by po niego sięgnąć:

  • Użycie jako klucz słownika lub element zbioru. Wszędzie tam, gdzie pojedyncza wartość nie potrafi uchwycić tego, czego potrzebujesz, może to zrobić frozenset wartości – „zbiór funkcji obsługiwanych przez ten sterownik”, „zbiór pinów używanych przez ten profil”.

  • Zabezpieczenie stałej. frozenset na poziomie modułu zawierający dozwolone nazwy nie może zostać przypadkowo zmodyfikowany przez wywołującego; zwykły set może. Preferuj frozenset dla wszystkiego, co po skonstruowaniu ma być tylko do odczytu.