Poznaliśmy już podstawowe typy danych w Pythonie (oraz różnie podstawowe funkcje), ale jeden niezwykle istotny typ danych nadal nam pozostaje. Jest nim słownik.
Słownik to taka kolekcja danych, która posiada klucze oraz przypisane im wartości. Na przykład słownik „kolor” może posiadać klucze „red”, „green” i „blue” oraz wartości dla czerwonego, zielonego i niebieskiego.
Tworzenie słownika – dict, literał
Tak jak listę możemy utworzyć przy pomocy funkcji list, tak i słownik możemy utworzyć przy pomocy funkcji dict. Przykład listy:
lst = list("hello")
print(lst)
#['h', 'e', 'l', 'l', 'o']
print(lst[0]) #h
print(lst[1]) #e
print(lst[2]) #l
print(lst[3]) #l
print(lst[4]) #0
Tutaj bierzemy napis hello i robimy z niego listę, która ma jako element każdą literę. Elementy możemy „łapać” przy pomocy znanych już nam indeksów, które liczymy od zera.
Ze słownikiem jest podobnie:
color = dict(red=100, green=200, blue=255)
print(color)
#{'red': 100, 'green': 200, 'blue': 255}
print(color["red"])
#100
print(color["green"])
#200
print(color["blue"])
#255
Tutaj podajemy nazwę klucza i jego wartość po znaku „=”. Do nazw klucza odnosimy się jak do indeksów, tylko zamiast liczb mamy napisy z nazwą klucza.
Można jednak, i tak się to robić zazwyczaj powinno, korzystać z literału słownika czyli zapisu z nawiasami wąsatymi {}:
color = {"red": 100, "green": 200, "blue": 255}
#{'red': 100, 'green': 200, 'blue': 255}
print(color["red"])
#100
print(color["green"])
#200
print(color["blue"])
#255
Tutaj panuje zasada: klucz (nasz napis, albo inny typ prosty, trzymajmy się napisu), dwukropek, wartość (dowolny typ danych), przecinek, następna para klucz-wartość.
Możemy nawet sobie zostawić przecinek na końcu, jest to dość częsta praktyka, gdy myślimy o ewentualnym późniejszym dopisywaniu czegoś do słownika:
color = {"red": 100, "green": 200, "blue": 255,}
#{'red': 100, 'green': 200, 'blue': 255}
print(color["red"])
#100
print(color["green"])
#200
print(color["blue"])
#255
Choć edytor PyCharm podkreśla to, jako złamanie jakiejś zasady PEP dotyczącej czystego kodu Pythona, to taki kod się wykona. Nie można natomiast o tych przecinkach zapomnieć.
Ale, używając literału, możemy sobie ten słownik „rozbić” na kilka linijek, aby kod był bardziej czytelny:
color = {"red": 100,
"green": 200,
"blue": 255}
#{'red': 100, 'green': 200, 'blue': 255}
print(color["red"])
#100
print(color["green"])
#200
print(color["blue"])
#255
Możemy nawet rozbić to tak:
color = {
"red": 100,
"green": 200,
"blue": 255
}
#{'red': 100, 'green': 200, 'blue': 255}
print(color["red"])
#100
print(color["green"])
#200
print(color["blue"])
#255
To już zależy od naszych upodobań. Ważne, aby pamiętać o zasadach pisania składni. Klucz, dwukropek, wartość, przecinek.
Tworzenie słownika – fromkeys, comprehension
A co jeżeli chcemy stworzyć słownik w pewnym sensie domyślny, zawierający klucze i jakąś domyślną wartość? Na przykład chcemy, aby zawierał klucze red, green i blue, ale wartości tych nie znamy, więc ustawmy je sobie na razie na zero?
Można to zrobić ręcznie:
color = {
"red": 0,
"green": 0,
"blue": 0
}
#{'red': 0, 'green': 0, 'blue': 0}
print(color["red"])
#0
print(color["green"])
#0
print(color["blue"])
#0
Problem polega na tym, że się powtarzamy, a programiści nie lubią się powtarzać. Jeżeli pamiętamy coś takiego jak list comprehension, oto przykład pierwszy z brzegu:
lst = [1,2,3,4,5]
lst2 = [num*2 for num in lst]
print(lst2)
#[2, 4, 6, 8, 10]
Tutaj ładujemy liczbę razy dwa dla każdej liczby z listy pierwszej. Jeżeli to pamiętamy to możemy chcieć coś podobnego zastosować w naszym słowniku, tylko tutaj mamy do czynienia z parami klucz-wartość.
My chcemy klucze mieć „red”, „green” i „blue” zaś wartość domyślnie jako 0. Jak to osiągnąć?
keys = ["red", "green", "blue"]
color = { key: 0 for key in keys}
print(color)
#{'red': 0, 'green': 0, 'blue': 0}
Tutaj tworzymy listę keys zawierającą nasze klucze. Następnie piszemy: zrób mi słownik zawierający klucz z wartością 0 dla każdego klucza w liście keys.
Można to zrobić jeszcze inaczej, metodą fromkeys:
keys = ["red", "green", "blue"]
color = {}.fromkeys(keys, 0)
print(color)
#{'red': 0, 'green': 0, 'blue': 0}
Metoda fromkeys bieerze listę kluczy oraz wartość domyślną. Wywołana jest na literalne pustego słownika {}. Podobne to trochę do metody join, zupełnie innej, ale ją też przypomnę:
keys = ["red", "green", "blue"]
text = "".join(keys)
print(text)
#redgreenblue
Ona tutaj na pustym napisie skleiła nam elementy listy do tekstu. Oczywiście możemy tam podać spację, dostaniemy elementy oddzielone po spacji. Tak tylko przypominam.
Wracając do fromkeys, możemy tego jeszcze tak użyć, zwłaszcza przy tak krótkiej liście kluczy:
color = {}.fromkeys(["red", "green", "blue"], 0)
print(color)
#{'red': 0, 'green': 0, 'blue': 0}
To już nasze własne upodobanie. Fromkeys bierze listę kluczy i wartość domyślną. Wartość może też być innego typu, na przykład tekst:
person = {}.fromkeys(["first_name", "last_name", "age"], "unknown")
print(person)
#{'first_name': 'unknown', 'last_name': 'unknown', 'age': 'unknown'}
Tutaj mamy słownik person i klucze first_name, last_name oraz age, z wartością domyślną „unknown” (po angielsku – nieznane).
A czy lista naszych kluczy to zawsze musi być lista? Czy ona też nie może być np. napisem?
Załóżmy, że chcemy mieć słownik samogłosek „aeiou”. Będziemy liczyć ilość samogłosek w danym tekście i każda będzie miała swój licznik. Po to aby potem wypisać ile razy padło 'a’ a ile 'u’ i tak dalej. Chcemy na początek podać im wszystkim wartość domyślną 0.
Możemy użyć listy:
vowels = {}.fromkeys(['a', 'e', 'i', 'o', 'u'], 0)
print(vowels)
#{'a': 0, 'e': 0, 'i': 0, 'o': 0, 'u': 0}
Taki zapis już się robi problematyczny, mało czytelny, zaś pamiętamy przecież, że napis to „pod spodem” też w zasadzie lista typu znakowego, lista znaków. Możemy zatem zrobić tak:
vowels = {}.fromkeys("aeiou", 0)
print(vowels)
#{'a': 0, 'e': 0, 'i': 0, 'o': 0, 'u': 0}
Jak najbardziej działa.
Korzystanie ze słownika – indeks, get, in
Do elementów słownika możemy odnosić się po indeksie, co już widzieliśmy:
color = {
"red": 0,
"green": 0,
"blue": 0
}
#{'red': 0, 'green': 0, 'blue': 0}
print(color["red"])
#0
print(color["green"])
#0
print(color["blue"])
#0
Możemy też przypisywać w ten sposób elementy do zmiennych:
color = {
"red": 100,
"green": 200,
"blue": 255
}
#{'red': 100, 'green': 200, 'blue': 255}
red = color["red"]
green = color["green"]
blue = color["blue"]
print(red, green, blue)
#100 200 255
Co jednak, gdybyśmy zapytali o klucz, który nie istnieje?
color = {
"red": 100,
"green": 200,
"blue": 255
}
#{'red': 100, 'green': 200, 'blue': 255}
red = color["red"]
green = color["green"]
blue = color["blue"]
alpha = color["alpha"]
print(red, green, blue, alpha)
Ten kod nawet się nie wykona do naszego polecenia print. Linijkę wyżej pytamy o klucz „apha”, który nie istnieje. Co możemy zrobić?
Możemy do aplha przypisać domyślne 1, następnie sprawdzić, czy taki klucz istnieje i jeżeli istnieje, przypisać do niego wartość, już nie domyślną. Pokraczna logika, ale pokazuje jak możemy sprawdzić, czy dany klucz istnieje (poprzez słówko in):
color = {
"red": 100,
"green": 200,
"blue": 255
}
#{'red': 100, 'green': 200, 'blue': 255}
red = color["red"]
green = color["green"]
blue = color["blue"]
alpha = 1
if "alpha" in color:
alpha = color["alpha"]
print(red, green, blue, alpha)
#100 200 255 1
Tutaj do alpha przypisaliśmy sobie wartość 1, potem sprawdzamy, czy taki klucz istnieje, jeżeli tak, do alpha przypisujemy tę wartość, błędu odnoszenia się do nieistniejącego klucza nie ma. Sprawdźmy to:
color = {
"red": 100,
"green": 200,
"blue": 255,
"alpha": 0.7
}
#{'red': 100, 'green': 200, 'blue': 255}
red = color["red"]
green = color["green"]
blue = color["blue"]
alpha = 1
if "alpha" in color:
alpha = color["alpha"]
print(red, green, blue, alpha)
#100 200 255 0.7
Działa jak należy. Przy okazji poznaliśmy słówko kluczowe in. Możemy nim sprawdzać obecność elementów także w liście albo napisie:
print(1 in [1,2,3])
#True
print("H" in "Hello World")
#True
print("abc" in {"abc": 123})
#True
Tutaj sprawdzamy, czy liczba 1 istnieje w liście, litera „H” w napisie oraz klucz „abc” w słowniku. Ale wracając do naszego przykładu: czy istnieje sposób „bezpiecznego” wyciągania wartości ze słownika, jeżeli nie mamy pewności czy dany klucz istnieje? A jednocześnie łatwiejszy niż to:
alpha = 1
if "alpha" in color:
alpha = color["alpha"]
Tak, istnieje. Nazywa się get. Podajemy nazwę klucza a gdy taki nie zostanie znaleziony = wartość domyślną. Oto przykład:
color = {
"red": 100,
"green": 200,
"blue": 255
}
#{'red': 100, 'green': 200, 'blue': 255}
red = color["red"]
green = color["green"]
blue = color["blue"]
alpha = color.get("alpha", 1)
print(red, green, blue, alpha)
#100 200 255 1
Tutaj prosimy o wartość pod kluczem alpha, ale jeżeli nie zostanie znaleziona, prosimy o wartość 1 zamiast błędu. Analogicznie możemy poprosić o nasze kolory, a gdyby z jakichś względów klucz był nieobecny, o wartość 0:
color = {
"red": 100,
"green": 200,
}
#{'red': 100, 'green': 200}
red = color.get("red", 0)
green = color.get("green", 0)
blue = color.get("blue", 0)
alpha = color.get("alpha", 1)
print(red, green, blue, alpha)
#100 200 0 1
Get oznacza – sprawdź czy klucz istnieje, jeżeli tak, zwróć wartość pod tym kluczem, jeżeli nie – zwróć tę drugą, domyślną wartość, jaką podałem. Jest to bezpieczny sposób wyciągania danych ze słownika bez martwienia się o to, czy dany klucz istnieje.
Używanie słownika – iterowanie
Po słowniku możemy iterować czyli przechodzić w pętli, jak po liście czy napisie. Przechodzenie po napisie:
msg = "Hello"
for letter in msg:
print(letter)
Przechodzenie po liście:
lst = [1,2,3,4,5]
for num in lst:
print(num)
Przy słowniku sytuacja jest trochę bardziej skomplikowana, bo musimy zdecydować po czym chcemy przechodzić – po kluczach, wartościach a może obu?
Po kluczach:
color = {
"red": 100,
"green": 200,
"blue": 255
}
for key in color.keys():
print(key)
Tutaj przechodzimy po kluczach. Warto dodać, że color.keys() daję nam tak naprawdę listę kluczy:
color = {
"red": 100,
"green": 200,
"blue": 255
}
print(color.keys())
#dict_keys(['red', 'green', 'blue'])
Istnieje też coś takiego jak values, co daje nam listę wartości:
color = {
"red": 100,
"green": 200,
"blue": 255
}
print(color.values())
#dict_values([100, 200, 255])
Po niej też możemy przechodzić w pętli:
color = {
"red": 100,
"green": 200,
"blue": 255
}
for value in color.values():
print(value)
Dostaniemy tutaj te liczby będące wartościami słownika. Istnieje też coś takiego jak items, czyli pary klucz-wartość:
color = {
"red": 100,
"green": 200,
"blue": 255
}
print(color.items())
#dict_items([('red', 100), ('green', 200), ('blue', 255)])
Jak widać mamy listę krotek (takich niemutowalnych list) gdzie pierwsza to klucz, druga to wartość. Aby przejść po jednych i drugich możemy je złapać do jednej zmiennej i odnosić się po indeksie, ale to mało przyjemne:
color = {
"red": 100,
"green": 200,
"blue": 255
}
for keyval in color.items():
print(keyval[0], keyval[1])
#red 100
#green 200
#blue 255
Albo możemy je „rozpakować” czyli dać dwie zmienne po przecinku, które mają „wyłapać” klucz i wartość:
color = {
"red": 100,
"green": 200,
"blue": 255
}
for key, val in color.items():
print(f"{key} = {val}")
# red = 100
# green = 200
# blue = 255
Dla przypomnienia dodam, że listę albo tuplę (krotkę) można też nazwać takim słownikiem, który jest uporządkowany i jego kluczami są indeksy, zaczynające się od 0. I podobny efekt możemy osiągnąć używając enumerate, które zwraca nam indeks i wartość:
lst = ["a", "b", "c"]
for idx, val in enumerate(lst):
print(f"{idx} = {val}")
# 0 = a
# 1 = b
# 2 = c
Podobnie jest w napisach, które też są listami:
for idx, val in enumerate("hello"):
print(f"{idx} = {val}")
# 0 = h
# 1 = e
# 2 = l
# 3 = l
# 4 = o
Tutaj też mamy do czynienia z „rozpakowywaniem” do dwóch zmiennych, oddzielonych przecinkiem. Oczywiście rozpakowywać nie musimy:
for idxval in enumerate("hello"):
print(idxval)
# (0, 'h')
# (1, 'e')
# (2, 'l')
# (3, 'l')
# (4, 'o')
Tak tylko o tym wspominam, bo to warte uwagi. Podsumowując, można przechodzić w pętli po kluczach słownika (keys), wartościach (values) oraz parach klucz-wartość (items). Czy chcemy je sobie rozpakowywać do dwóch zmiennych, czy używać jako jedną tuplę, to już nasze upodobanie:
color = {
"red": 100,
"green": 200,
"blue": 255
}
for keyval in color.items():
print(keyval[0], keyval[1])
#red 100
#green 200
#blue 255
for key, val in color.items():
print(f"{key} = {val}")
# red = 100
# green = 200
# blue = 255
Dict comprehension – podobnie jak w listach
Poznajmy nowy operator, który służy do podnoszenia do potęgi, jednocześnie przypominając sobie jak działa list comprehension (składanie list). To się nam przyda do bardzo podobnego tworzenia słowników:
lst = [1,2,3,4,5]
squared = [num ** 2 for num in lst]
print(squared)
#[1, 4, 9, 16, 25]
Mamy tutaj listę liczb od 1 do 5 i drugą listę squared, gdzie mają znajdować się te liczby podniesione do kwadratu. Do tego służy operator podnoszenia do potęgi „**”. Mamy tam zapisane „num do kwadratu dla każdego num w liście lst”.
Analogicznie możemy zrobić słownik, gdzie kluczami będą liczby od 1 do 5 (liczba też może być kluczem, nie tylko napis, „pod spodem” tak de facto nasz napis jest przechowywany jako hash, czyli liczba, ale tego wiedzieć nie musimy), którego wartościami będą liczby podniesione do kwadratu.
Klucze – liczby od 1 do 5, wartości – te same liczby do kwadratu podniesione. Do dzieła:
lst = [1,2,3,4,5]
my_dict = { num: num ** 2 for num in lst}
print(my_dict)
#{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
Tutaj piszemy – „kluczem ma być num a wartością num do kwadratu dla każdego num w liście lst”.
Zróbmy coś innego, co już robiliśmy wcześniej – klucze to red, green i blue zaś wartość to 0:
keys = ["red", "green", "blue"]
color = {key: 0 for key in keys}
print(color)
#{'red': 0, 'green': 0, 'blue': 0}
Tutaj piszemy – „kluczem ma być key, zaś wartością 0 dla każdego klucza w liście keys”. Proste.
Teraz zróbmy coś innego: mamy listę imion. Kluczami będą te imiona, zaś wartościami ich długość. Długość tekstu zwraca nam raczej już nam znana funkcja len. Jak się do tego zabrać?
names = ["John", "Jane", "Jim"]
my_dict = {name: len(name) for name in names}
print(my_dict)
#{'John': 4, 'Jane': 4, 'Jim': 3}
Tutaj piszemy – „kluczem ma być name zaś wartością długość name dla każdego name w liście names”. Dostajemy słownik, gdzie imię jest kluczem zaś jego długość wartością.
Aby pokazać „pełnię mocy” składania słowników, dodajmy sobie jeszcze warunek, że imię (klucz w słowniku) ma być małą literą zapisane:
names = ["John", "Jane", "Jim"]
my_dict = {name.lower() : len(name) for name in names}
print(my_dict)
#{'john': 4, 'jane': 4, 'jim': 3}
Jak widać dict comprehension to naprawdę potężne narzędzie. Tutaj napisaliśmy – „kluczem name do małej litery, wartością name wrzucone w funkcję len, dla każdego name w liście names”. Myślę, że tutaj warto zakończyć ten wpis, ale chciałbym jeszcze, w ramach ciekawostki powiedzieć nieco o tych haszach, które wspomniałem.
Powiedziałem, że jako klucz listy może być liczba zamiast napisu. Dzieje się tak, ponieważ „pod spodem” przechowywane są hasze a nie napisy. Algorytm haszujący jest dość zaawansowany, tak, aby nie było kolizji (więcej niż 1 napis sprowadzony do tej samej wartości liczbowej) natomiast jak wyglądają takie proste hasze, to sobie podejrzeć możemy poprzez funkcję hash() wbudowaną w Pythona, jednocześnie ćwicząc dict comprehension:
names = ["John", "Jane", "Jim"]
my_dict = {name : hash(name) for name in names}
print(my_dict)
#{'John': 9172000307927275220, 'Jane': 1786110096500258032, 'Jim': -5794710668809661636}
Tutaj mamy słownik, gdzie kluczem jest imię, wartością jego hash. Ten hasz nie musi zawsze wyglądać tak samo i zaznaczam, funkcja hash jest funkcją uproszczoną w pewnym sensie.
Natomiast szokiem może być, że np. hasła w różnych serwisach internetowych nie są przechowywane jako tekst, ale też jako hasze. I gdy wpisujemy hasło, aby się zalogować, program zamienia nam to hasło na hasz i porównuje z haszem w bazie danych. Dzięki temu, gdyby doszło do przecieku, wyciekają dane użytkowników, ale zamiast haseł mamy hasze, które w założeniu mają być nie do odszyfrowania (choć nie są).
Co zatem robią hakerzy, aby ułatwić sobie pracę? Tzw. rainbow tables. Czyli listy popularnych haseł, które sami haszują. Potem, gdy jest jakiś wyciek danych, mogą zobaczyć, czy do tego hasza akurat hasła nie mają.
Możemy zabawić się w takiego hakera, ćwicząc dict comprehension przy okazji. Zróbmy słownik z popularnymi hasłami oraz ich haszami. Zakładamy dla uproszczenia, że funkcja wbudowana hash() to ta sama, której używają na serwisach internetowych.
passwords = ["boss", "admin", "master"]
rainbow_table = {pswd : hash(pswd) for pswd in passwords}
print(rainbow_table)
#{'boss': -2853056395963336937, 'admin': 5532808455450962438, 'master': 6603611760889057813}
Mamy tutaj listę passwords z popularnymi hasłami oraz słownik rainbow_table, gdzie kluczem jest hasło zaś wartością jego hasz dla każdego hasła w tej liście.
Teraz, gdy zobaczymy wyciek danych z hasłem, którego hasz to „5532808455450962438” to wiemy, że hasło to „boss”.
To oczywiście pewne uproszczenie, funkcja hash różni się trochę od tej używanej powszechnie, zaś te listy haseł są naprawdę długie, natomiast już to powinno obrazować, jak potężny jest Python jako język skryptowy.
Nie wiem czy zniechęciłem, czy zachęciłem do dalszej nauki, ale taka ciekawostka – Python używany jest nie tylko do pisania programów z zaawansowaną logiką, ale także do różnych skryptów, ludzi najczęściej dobrze w zagadnieniach informatycznych obeznanych, którzy wykorzystują prostą składnię Pythona do szybkiego wykonywania różnych rzeczy, automatyzacji.
Jest też wykorzystywany do sztucznej inteligencji, analizy danych i w wielu innych dziedzinach. Tak czy inaczej – do nauki Pythona jeszcze wrócimy. Natomiast brakowało mi jakiegoś „praktycznego” przykładu na koniec, więc oto on, rainbow table używając dict comprehension.