Практика · Словники¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що в лекції, — телефонна книга. Тут ми руками зробимо все, про що йшлося:
- створимо книгу, знайдемо, додамо й видалимо запис;
- подивимось, чим
[]відрізняється від.get()— і зловимо справжнійKeyError; - побачимо, що
keys()— це вікно, а не копія; - порахуємо частоти слів через
.get()і перевіримо себе черезcollections.Counter; - замінимо довгий ланцюжок умов однією таблицею відповідностей;
- переконаємось, що список ключем бути не може, а кортеж — може;
- подивимось на порядок вставки й на те, як словник росте в пам'яті.
Запускай клітинки згори вниз. Дві клітинки навмисно падають — це не помилка зошита, а навчальний матеріал: traceback теж треба вміти читати.
1 · Створюємо телефонну книгу¶
Фігурні дужки, двокрапка між ключем і значенням, кома між парами. Ключ тут — ім'я, значення — номер.
телефонна_книга = {
"Аня": "067-111-22-33",
"Богдан": "050-444-55-66",
"Галя": "063-777-88-99",
}
print("книга:", телефонна_книга)
print("тип:", type(телефонна_книга))
print("записів:", len(телефонна_книга))
книга: {'Аня': '067-111-22-33', 'Богдан': '050-444-55-66', 'Галя': '063-777-88-99'}
тип: <class 'dict'>
записів: 3
2 · Знайти запис¶
У квадратних дужках стоїть не номер позиції, а ключ. Оператор in перевіряє наявність
ключа — і робить це так само швидко, як і саме звертання.
print('телефонна_книга["Богдан"] ->', телефонна_книга["Богдан"])
print('"Галя" in телефонна_книга ->', "Галя" in телефонна_книга)
print('"Петро" in телефонна_книга ->', "Петро" in телефонна_книга)
# in перевіряє саме КЛЮЧІ, а не значення — ось доказ
print('"067-111-22-33" in телефонна_книга ->', "067-111-22-33" in телефонна_книга)
телефонна_книга["Богдан"] -> 050-444-55-66 "Галя" in телефонна_книга -> True "Петро" in телефонна_книга -> False "067-111-22-33" in телефонна_книга -> False
3 · Додати й перезаписати — це одна й та сама дія¶
У списку присвоєння за неіснуючим індексом падає. У словнику воно створює запис. А якщо ключ уже є — значення тихо замінюється, і другого «Богдана» не з'явиться ніколи.
телефонна_книга["Ніна"] = "096-222-33-44" # нового ключа не було — запис створився
print("записів після додавання Ніни:", len(телефонна_книга))
телефонна_книга["Ніна"] = "096-000-00-00" # ключ уже є — старе значення затерто
print("записів після перезапису: ", len(телефонна_книга))
print("номер Ніни тепер: ", телефонна_книга["Ніна"])
записів після додавання Ніни: 4 записів після перезапису: 4 номер Ніни тепер: 096-000-00-00
4 · Перша навмисна помилка: KeyError¶
Ключа «Петро» в книзі немає. Квадратні дужки на це реагують єдиним способом — зупиняють програму. Прочитай traceback: останній рядок називає і тип помилки, і сам ключ.
телефонна_книга["Петро"]
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) Cell In[4], line 1 ----> 1 телефонна_книга["Петро"] KeyError: 'Петро'
5 · .get() — те саме питання, інша реакція¶
.get() не падає. Без другого аргументу він повертає None, з другим — те, що ти передав.
Важливо: словник при цьому не змінюється, значення за замовчуванням ніде не зберігається.
print("get('Петро') ->", телефонна_книга.get("Петро"))
print("get('Петро', 'немає') ->", телефонна_книга.get("Петро", "немає"))
print("get('Аня') ->", телефонна_книга.get("Аня"))
print("а сама книга не змінилась — записів:", len(телефонна_книга),
"· 'Петро' in книзі ->", "Петро" in телефонна_книга)
get('Петро') -> None
get('Петро', 'немає') -> немає
get('Аня') -> 067-111-22-33
а сама книга не змінилась — записів: 4 · 'Петро' in книзі -> False
6 · setdefault — узяти або одразу покласти¶
Ось де плутанина з .get() найчастіша. setdefault змінює словник: якщо ключа немає,
він його вставляє. Це зручно, коли треба гарантувати, що під ключем уже щось лежить.
запаси = {"яблука": 5}
print("setdefault('яблука', 0) ->", запаси.setdefault("яблука", 0)) # ключ був — 5
print("setdefault('груші', 0) ->", запаси.setdefault("груші", 0)) # ключа не було — 0
print("запаси після двох викликів:", запаси)
# для порівняння: get нічого не вставляє
print("get('сливи', 0) ->", запаси.get("сливи", 0))
print("запаси після get:", запаси)
setdefault('яблука', 0) -> 5
setdefault('груші', 0) -> 0
запаси після двох викликів: {'яблука': 5, 'груші': 0}
get('сливи', 0) -> 0
запаси після get: {'яблука': 5, 'груші': 0}
7 · Видалення: del і pop¶
Різниця одна: чи потрібне тобі значення, яке зникає. pop його повертає, del — ні.
І тільки pop уміє не падати на відсутньому ключі.
del телефонна_книга["Ніна"] # просто прибрати
print("після del:", list(телефонна_книга))
прибраний_номер = телефонна_книга.pop("Галя") # прибрати й забрати значення
print("pop повернув:", прибраний_номер)
print("після pop:", list(телефонна_книга))
# запасний варіант рятує від KeyError на неіснуючому ключі
print("pop('Петро', 'такого не було') ->",
телефонна_книга.pop("Петро", "такого не було"))
після del: ['Аня', 'Богдан', 'Галя']
pop повернув: 063-777-88-99
після pop: ['Аня', 'Богдан']
pop('Петро', 'такого не було') -> такого не було
8 · keys(), values(), items() — вікна, а не копії¶
Подивись на типи: це не списки. Це представлення — живі вікна в той самий словник.
Копію, яка застигне назавжди, доводиться робити руками через list(...).
книга = {"Аня": "067-111-22-33", "Богдан": "050-444-55-66"}
представлення = книга.keys() # вікно
копія = list(книга.keys()) # знімок, зроблений просто зараз
print("тип представлення:", type(представлення))
print("тип копії: ", type(копія))
print()
print("до зміни · представлення:", представлення)
print("до зміни · копія: ", копія)
книга["Галя"] = "063-777-88-99" # міняємо словник ПІСЛЯ того, як обидва створені
print()
print("після зміни · представлення:", представлення, "<- оновилось саме")
print("після зміни · копія: ", копія, "<- застрягла в минулому")
тип представлення: <class 'dict_keys'> тип копії: <class 'list'> до зміни · представлення: dict_keys(['Аня', 'Богдан']) до зміни · копія: ['Аня', 'Богдан'] після зміни · представлення: dict_keys(['Аня', 'Богдан', 'Галя']) <- оновилось саме після зміни · копія: ['Аня', 'Богдан'] <- застрягла в минулому
Те саме з .items(): він повертає пари як кортежі. Обходити їх у циклі ми навчимося
в темі 12 — поки що досить побачити, що саме там лежить.
print("items():", книга.items())
print("перший елемент items():", list(книга.items())[0])
print("а це вже звичайний кортеж:", type(list(книга.items())[0]))
items(): dict_items([('Аня', '067-111-22-33'), ('Богдан', '050-444-55-66'), ('Галя', '063-777-88-99')])
перший елемент items(): ('Аня', '067-111-22-33')
а це вже звичайний кортеж: <class 'tuple'>
9 · update() і об'єднання через |¶
update() змінює той словник, якому його сказали. Оператор | (з Python 3.9) нічого
не чіпає — він робить новий словник. При конфлікті ключів перемагає правий операнд.
за_замовчуванням = {"тема": "світла", "мова": "uk", "розмір": 14}
вибір_користувача = {"мова": "en", "тема": "темна"}
підсумок = за_замовчуванням | вибір_користувача # новий словник
print("підсумок: ", підсумок)
print("за_замовчуванням цілий:", за_замовчуванням)
копія_налаштувань = dict(за_замовчуванням) # щоб не псувати оригінал
копія_налаштувань.update(вибір_користувача) # а ось це змінює на місці
print("після update: ", копія_налаштувань)
print("update дав те саме, що | :", копія_налаштувань == підсумок)
підсумок: {'тема': 'темна', 'мова': 'en', 'розмір': 14}
за_замовчуванням цілий: {'тема': 'світла', 'мова': 'uk', 'розмір': 14}
після update: {'тема': 'темна', 'мова': 'en', 'розмір': 14}
update дав те саме, що | : True
10 · Підрахунок частот через .get()¶
Класична задача: скільки разів трапилось кожне слово. Ідіома тут одна на всі випадки:
лічильник[слово] = лічильник.get(слово, 0) + 1
Перше входження слова дає get -> 0, отже записується 1. Друге бачить уже 1 і робить 2.
Спершу зробимо кілька кроків вручну, щоб було видно кожен.
лічильник = {}
# крок 1: слова «кіт» ще немає — get поверне запасний 0
лічильник["кіт"] = лічильник.get("кіт", 0) + 1
print("після 'кіт': ", лічильник)
# крок 2: нове слово
лічильник["пес"] = лічильник.get("пес", 0) + 1
print("після 'пес': ", лічильник)
# крок 3: «кіт» уже є — get поверне 1, запишемо 2
лічильник["кіт"] = лічильник.get("кіт", 0) + 1
print("після 'кіт': ", лічильник)
# крок 4: і ще раз
лічильник["кіт"] = лічильник.get("кіт", 0) + 1
print("після 'кіт': ", лічильник)
після 'кіт': {'кіт': 1}
після 'пес': {'кіт': 1, 'пес': 1}
після 'кіт': {'кіт': 2, 'пес': 1}
після 'кіт': {'кіт': 3, 'пес': 1}
Забігання наперед: той самий підрахунок у циклі¶
Циклів у нас іще не було — вони в темі 12. Але рядок усередині циклу той самий, що вище, слово в слово. Просто тепер його виконує не програміст руками, а Python по черзі для кожного елемента.
І одразу головна перевірка практики: наш результат має точно збігтися з тим,
що дає стандартний collections.Counter. Ніякої магії всередині бібліотеки немає —
там та сама хеш-таблиця й та сама ідея.
from collections import Counter
слова = "кіт пес кіт миша пес кіт риба".split()
наш_підрахунок = {}
for слово in слова: # забігання наперед: тема 12
наш_підрахунок[слово] = наш_підрахунок.get(слово, 0) + 1
бібліотечний = dict(Counter(слова))
print("наш: ", наш_підрахунок)
print("Counter: ", бібліотечний)
assert наш_підрахунок == бібліотечний, "наш підрахунок розійшовся з Counter!"
print("✅ збігається")
наш: {'кіт': 3, 'пес': 2, 'миша': 1, 'риба': 1}
Counter: {'кіт': 3, 'пес': 2, 'миша': 1, 'риба': 1}
✅ збігається
11 · Словник замість довгого ланцюжка умов¶
Умов ми ще не вчили, але вже можемо обійтися без них. Коли задача звучить як
«такому-то коду відповідає такий-то текст», найкоротший і найшвидший розв'язок —
таблиця відповідностей плюс одне звертання з .get().
пояснення_коду = {
200: "усе гаразд",
301: "сторінка переїхала назавжди",
404: "сторінки немає",
418: "я чайник",
500: "зламався сервер",
}
# одне звертання замість п'яти перевірок; невідомий код не ламає програму
print("200 ->", пояснення_коду.get(200, "невідомий код"))
print("404 ->", пояснення_коду.get(404, "невідомий код"))
print("418 ->", пояснення_коду.get(418, "невідомий код"))
print("777 ->", пояснення_коду.get(777, "невідомий код"))
# і скільки б рядків у таблиці не було — робота та сама
print("кодів у таблиці:", len(пояснення_коду), "· кроків пошуку завжди 1")
200 -> усе гаразд 404 -> сторінки немає 418 -> я чайник 777 -> невідомий код кодів у таблиці: 5 · кроків пошуку завжди 1
12 · Друга навмисна помилка: список ключем бути не може¶
Адреса запису обчислюється з ключа. Якщо ключ можна змінити — адреса «протухне», і значення
загубиться. Python не дає цьому статись: у списку метод __hash__ дорівнює None.
{["Київ", "Хрещатик"]: "01001"}
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[14], line 1 ----> 1 {["Київ", "Хрещатик"]: "01001"} TypeError: unhashable type: 'list'
13 · А кортеж — може¶
Кортеж незмінний, тому його хеш рахується один раз і назавжди. Це стандартний спосіб зробити ключ із кількох частин.
поштові_індекси = {
("Київ", "Хрещатик"): "01001",
("Львів", "Ринок"): "79008",
("Одеса", "Дерибасівська"): "65026",
}
print("ключ із двох частин:", поштові_індекси[("Львів", "Ринок")])
# хеш кортежа з рядками змінюється між запусками інтерпретатора — це навмисно
print("hash кортежа:", hash(("Львів", "Ринок")))
print("hash числа 42:", hash(42), "· число хешує саме себе")
# але кортеж зі списком усередині вже не хешується
try:
hash((1, [2]))
except TypeError as помилка:
print("hash((1, [2])) ->", type(помилка).__name__, "-", помилка)
ключ із двох частин:
79008 hash кортежа: -4124586230098368815 hash числа 42: 42 · число хешує саме себе hash((1, [2])) -> TypeError - unhashable type: 'list'
14 · Порядок вставки гарантований — і це саме порядок вставки¶
З Python 3.7 словник зберігає той порядок, у якому ключі додавали. Але саме вставки: якщо ключ видалити й додати знову, він поїде в кінець, а не повернеться на старе місце.
черга = {"Аня": 1, "Богдан": 2, "Галя": 3}
print("початок: ", list(черга))
черга["Дмитро"] = 4
print("додали Дмитра: ", list(черга))
del черга["Богдан"]
print("видалили Богдана:", list(черга))
черга["Богдан"] = 5
print("додали знову: ", list(черга), "<- Богдан у кінці!")
assert list(черга) == ["Аня", "Галя", "Дмитро", "Богдан"], "порядок вставки не збігся"
print("✅ порядок саме такий, як очікували")
початок: ['Аня', 'Богдан', 'Галя'] додали Дмитра: ['Аня', 'Богдан', 'Галя', 'Дмитро'] видалили Богдана: ['Аня', 'Галя', 'Дмитро'] додали знову: ['Аня', 'Галя', 'Дмитро', 'Богдан'] <- Богдан у кінці! ✅ порядок саме такий, як очікували
15 · Як словник росте в пам'яті¶
Хеш-таблиця не дає собі заповнитись більш ніж на дві третини: інакше колізії стають
задовгими. Коли межу перейдено, створюється більша таблиця й усі ключі перехешовуються.
sys.getsizeof показує ці стрибки прямо.
import sys
словник = {}
попередній_розмір = sys.getsizeof(словник)
print("порожній словник:", попередній_розмір, "байтів")
for номер in range(1, 25): # забігання наперед: тема 12
словник["ключ%02d" % номер] = номер
розмір = sys.getsizeof(словник)
if розмір != попередній_розмір:
подія = "перша таблиця на 8 комірок" if номер == 1 else "перехешування"
print("на %2d-му ключі розмір став %3d байтів <- %s"
% (номер, розмір, подія))
попередній_розмір = розмір
print("усього ключів:", len(словник), "· підсумковий розмір:", sys.getsizeof(словник))
порожній словник: 64 байтів на 1-му ключі розмір став 184 байтів <- перша таблиця на 8 комірок на 6-му ключі розмір став 272 байтів <- перехешування на 11-му ключі розмір став 464 байтів <- перехешування на 22-му ключі розмір став 832 байтів <- перехешування усього ключів: 24 · підсумковий розмір: 832
16 · Дрібниця, яка ловить усіх: 1, 1.0 і True¶
Правило Python: якщо два значення рівні, їхні хеші теж рівні. А 1 == 1.0 == True.
Отже для словника це один ключ, записаний трьома способами.
print("hash(1) =", hash(1), "· hash(1.0) =", hash(1.0), "· hash(True) =", hash(True))
плутанина = {1: "ціле"}
плутанина[1.0] = "дробове"
плутанина[True] = "логічне"
print("думали, буде три ключі, а вийшов:", плутанина)
print("довжина:", len(плутанина))
assert len(плутанина) == 1, "несподівано: ключів більше одного"
print("✅ один ключ, значення перезаписалось двічі")
hash(1) = 1 · hash(1.0) = 1 · hash(True) = 1
думали, буде три ключі, а вийшов: {1: 'логічне'}
довжина: 1
✅ один ключ, значення перезаписалось двічі
Завдання¶
🟢 Рівень 1¶
Візьми словник пояснення_коду з розділу 11 і додай до нього коди 403 («доступу немає»)
і 502 («поганий шлюз»). Потім видали код 418 через pop так, щоб отримати його
пояснення в змінну й надрукувати. Перевір assert len(пояснення_коду) == 6.
🟡 Рівень 2¶
Дано два словники цін:
базові = {"хліб": 32, "молоко": 41, "яйця": 78}
акційні = {"молоко": 35, "сир": 210}
Зроби новий словник підсумкові, у якому акційні ціни перекривають базові, а базові
лишаються недоторканими. Перевір трьома assert: що підсумкові["молоко"] == 35,
що базові["молоко"] == 41 і що в підсумковому чотири позиції.
🔴 Рівень 3¶
Напиши функцію перевернути(словник), яка робить новий словник «значення → ключ».
Подумай наперед про два питання й дай на них відповідь у коментарі:
- що станеться, якщо два різні ключі мали однакове значення;
- чому функція впаде, якщо серед значень є список — і як це полагодити одним словом.
Перевір на {"Аня": "067", "Богдан": "050"} і на {"а": 1, "б": 1}.