dict и set
Проблем
Кой е този потребител?
Потребителите на сайта са в списък. При всяко влизане търсиш имейла по потребителското име:
def find_email(users, username): for u in users: if u.username == username: return u.email return NoneС три потребителя това е мигновено. С 2 000 000 всяко влизане сравнява до 2 000 000 имена, а влизанията са хиляди в минута. Телефонен указател не се чете от първата страница. Отиваш направо на буквата.
Идеята
Речник: ключ → стойност
Искаш да питаш „кой е имейлът на georgiev?“ и да получиш отговор веднага. Колекцията за това се казва речник (map). Тя пази двойки: ключ и стойност. В Python речникът е вграден и се казва dict:
emails = {}emails["ivanov"] = "ivanov@example.com"emails["mpetrova"] = "mpetrova@example.com"emails["georgiev"] = "georgiev@example.com"
print(emails["georgiev"])print(emails.get("nobody"))print("Потребители:", len(emails))georgiev@example.comNoneПотребители: 3{}е празен речник.emails[ключ] = стойностслага двойка, аemails[ключ]връща стойността.get(ключ)също връща стойността, а за ключ, който го няма, даваNone.len(emails)е броят двойки. Не е нуженimport.- Типове не се пишат: в един речник може да има всякакви ключове и стойности. Ако искаш да кажеш какви са, има подсказка за типа:
emails: dict[str, str] = {}.
emails[ключ] за ключ, който го няма, не дава None, а спира програмата. Речникът може да се напише и наведнъж, с двойки ключ: стойност:
emails = {"ivanov": "ivanov@example.com", "mpetrova": "mpetrova@example.com"}print(emails["ivanov"])try: print(emails["nobody"])except KeyError as e: print(f"KeyError: {e}")ivanov@example.comKeyError: 'nobody'emails[ключ] не обхожда всички двойки, а намира мястото на ключа почти веднага, без значение колко са. Как го прави, е следващият урок.
Идеята
Складът: промени и проверки
Наличностите в склада: продукт → брой.
stock = {}stock["лаптоп"] = 4stock["мишка"] = 25stock["лаптоп"] = 3
print(stock["лаптоп"])print("монитор" in stock)print(stock.get("монитор", 0))del stock["мишка"]print(len(stock))3False01- Ключовете са уникални. Второто
stock["лаптоп"] = 3не добавя нов ред, а заменя 4 с 3. ключ in stockпита дали ключът е вътре.get(ключ, 0)връща стойността или 0, ако ключа го няма. Това пази отKeyError.del stock[ключ]маха двойката.
По-точно, за любопитните
- Стойностите могат да се повтарят: лаптоп и мишка може да са по 3. Ключовете не могат.
-
Може да питаш и
3 in stock.values(). Това обаче обхожда всички стойности, защото речникът е бърз само по ключ.
Пробвай
Колко са в менюто?
Какво ще отпечата програмата?
menu = {}menu["кафе"] = 2menu["чай"] = 2menu["кафе"] = 3print(len(menu), menu["кафе"])Вторият запис, menu["кафе"] = 3, заменя цената на кафето. Ключовете остават два, кафе и чай, а кафето вече е 3.
Идеята
Обхождане и редът
Цената на доставката по градове. items() дава двойките ключ и стойност, а for city, price in ги разделя на две имена:
delivery = {}delivery["София"] = 4.5delivery["Пловдив"] = 5.2delivery["Варна"] = 6.0delivery["Бургас"] = 6.0delivery["Русе"] = 5.8
for city, price in delivery.items(): print(f"{city}: {price:.2f} €")София: 4.50 €Пловдив: 5.20 €Варна: 6.00 €Бургас: 6.00 €Русе: 5.80 €Градовете излизат в реда, в който са добавени. dict пази реда на добавяне и от Python 3.7 това е обещано. :.2f печата цената с два знака след точката. Със същото присвояване и print(delivery):
delivery = {}delivery["София"] = 4.5delivery["Пловдив"] = 5.2delivery["Варна"] = 6.0delivery["Бургас"] = 6.0delivery["Русе"] = 5.8print(delivery){'София': 4.5, 'Пловдив': 5.2, 'Варна': 6.0, 'Бургас': 6.0, 'Русе': 5.8}Когато ти трябва азбучен ред, sorted(delivery) дава подреден списък с ключовете. Речникът не се променя:
delivery = {}delivery["София"] = 4.5delivery["Пловдив"] = 5.2delivery["Варна"] = 6.0delivery["Бургас"] = 6.0delivery["Русе"] = 5.8
for city in sorted(delivery): print(f"{city}: {delivery[city]:.2f} €")Бургас: 6.00 €Варна: 6.00 €Пловдив: 5.20 €Русе: 5.80 €София: 4.50 €По-точно, за любопитните
- Ако ти трябват само ключовете, обхождай
delivery. Ако ти трябват само стойностите, обхождайvalues():
for city in delivery: print(city, end=" ")print()
print(f"Средно: {sum(delivery.values()) / len(delivery):.2f} €")София Пловдив Варна Бургас РусеСредно: 5.50 €sum(...)събира числата, аlen(delivery)е броят двойки.- Редът на добавяне пази речникът, но не и множеството
setот края на урока.
Идеята
Броене
Колко пъти е поръчан всеки продукт? Ключът е продуктът, стойността е броят. При всяка поръчка вземаш стария брой (или 0) и добавяш 1:
orders = "кафе чай кафе вода кафе чай"counts = {}for item in orders.split(" "): counts[item] = counts.get(item, 0) + 1print(counts){'кафе': 3, 'чай': 2, 'вода': 1}Това е най-честата употреба на речник: броене на думи, гласове, грешки по вид, продажби по град. Продуктите излизат в реда, в който са видени за първи път.
orders = "кафе чай кафе вода кафе чай"counts = {}for item in orders.split(" "): counts[item] = counts.get(item, 0) + 1print(counts)Стъпка по стъпка, с думи
- 6 поръчки и празен речник.
- „кафе“ го няма: get(item, 0) дава 0, а присвояването добавя ключа с 1.
- „чай“ го няма: get(item, 0) дава 0, а присвояването добавя ключа с 1. Речникът пази реда на добавяне: новият ключ е последен.
- „кафе“ вече е вътре: get дава 1, а присвояването заменя стойността с 2.
- „вода“ го няма: get(item, 0) дава 0, а присвояването добавя ключа с 1. Речникът пази реда на добавяне: новият ключ е последен.
- „кафе“ вече е вътре: get дава 2, а присвояването заменя стойността с 3.
- „чай“ вече е вътре: get дава 1, а присвояването заменя стойността с 2.
- Всеки продукт е ключ веднъж, а стойността е колко пъти е поръчан.
По-точно, за любопитните
split(" ")от урока за текста разделя реда на думи.-
Броенето е толкова често, че в
collectionsима готов речник за него,Counter. За ключ, който го няма, той дава 0, а неKeyError:
from collections import Counter
orders = "кафе чай кафе вода кафе чай"counts = Counter(orders.split(" "))print(counts)print(counts["кафе"], counts["сок"])Counter({'кафе': 3, 'чай': 2, 'вода': 1})3 0Пробвай
Намери грешката
KeyError: 'кафе'На кой ред е грешката?
Избери реда с грешката.
counts[item] спира програмата с KeyError. Съобщението казва кой ключ липсва. Поправката е counts.get(item, 0) + 1.Идеята
set: всяко по веднъж
Колко различни хора са влезли в сайта днес? Ако някой влезе пет пъти, трябва да се брои веднъж. Колекция, която пази всяка стойност само по веднъж, се казва множество (set):
visits = ["ana@example.com", "boris@example.com", "ana@example.com", "vera@example.com", "boris@example.com"]visitors = set()for email in visits: visitors.add(email)print("Посещения:", len(visits))print("Различни посетители:", len(visitors))print("vera@example.com" in visitors)Посещения: 5Различни посетители: 3Trueset()е празно множество.{}не е: това е празен речник.addна стойност, която вече е вътре, не прави нищо. И в двата случая връщаNone, затова не казва дали е добавил:
seen = set()print(seen.add("F-101"))Noneстойност in visitorsе бърз катоinна речника. Отвътре множеството е като речник, в който пазиш само ключовете.- Индекси няма. Множеството отговаря само на въпроса „вътре ли е?“.
По-точно, за любопитните
- Множеството не пази ред. При текст редът се сменя дори между две пускания на една и съща програма, защото Python смята хеша на текста различно при всяко пускане. Затова се печата със
sorted(). Множество може да се напише и наведнъж, в{}със стойности:
tags = {"java", "sql", "git", "java"}print(len(tags))print(sorted(tags))
backend = {"java", "sql", "docker"}print(sorted(tags & backend))print(sorted(tags | backend))3['git', 'java', 'sql']['java', 'sql']['docker', 'git', 'java', 'sql']&оставя само общите елементи на две множества, а|ги обединява. Ще ти трябват в упражнението.
Пробвай
Повторена фактура
invoices = ["F-101", "F-102", "F-101", "F-103"]
seen = set()
for number in invoices:
if number seen:
print("Повторена фактура:", number)
seen.add(number)number in seen пита дали номерът вече е видян. add не казва нищо, затова първо питаш, после запомняш. Програмата отпечатва „Повторена фактура: F-101“.Упражнение
Речници в работа
Реши задачите в редактора си.
1. Таблица с потребители. Клас UserTable с методи add(username, email), get(username) и size(). Отвътре е речник. Добави ivanov, mpetrova и georgiev с имейли във вида ivanov@example.com и потърси mpetrova и nobody. В следващия урок ще направиш и своя хеш таблица, без dict.
mpetrova: mpetrova@example.comnobody: няма такъв потребителПотребители: 32. Честота на думите. Прочети ред текст, направи го с малки букви и преброй всяка дума. Отпечатай думите по азбучен ред.
Кафе с мляко и кафе без захар и кафебез: 1захар: 1и: 2кафе: 3мляко: 1с: 13. Документи по ключова дума. Клас DocumentIndex с методи add_document(doc_id, *keywords) и search(keyword), който връща списък с номерата на документите с тази дума. Отвътре е речник от списъци: за всяка дума списък от документи. Добави документ 1 с „ООП“ и „Java“, документ 2 със „SQL“ и „бази данни“, документ 3 с „Java“ и „колекции“. Потърси „Java“, „SQL“ и „Python“.
Java: [1, 3]SQL: [2]Python: []4. Мрежа от контакти. Клас Contact с име и имейл и клас ContactNetwork с методи add_contact(name, email), add_connection(a, b) (връзката е в двете посоки), friends_of(name), common(a, b) (общите познати) и email_of(name). Пази контактите в речник име → Contact, а връзките в речник име → множество от имена. friends_of и common връщат подреден списък. Свържи Иван с Мария и Иван с Петър.
Иван: ['Мария', 'Петър']Мария: ['Иван']Общи познати на Мария и Петър: ['Иван']Имейл на Петър: petar@example.comОще задачи
- Уникален идентификатор. Функция
generate_user_id(email)дава на всеки нов имейл номерU001,U002и т.н., а за вече видян имейл връща същия номер. Големи и малки букви в имейла не се различават. Вариант с хеш функция има в следващия урок. - Каталог на книги. Ключът е ISBN, например
978-954-28-0001-2, а стойността е заглавието. Добави, потърси и отпечатай всички книги, подредени по ISBN. - Организатор на пътуване. За всяка дестинация пази списък от дейности: „Париж“ → „Айфеловата кула“, „Лувъра“. Отпечатай програмата в реда, в който са добавени дестинациите. Трябва ли нещо повече от обикновен речник?
- Електронен речник. Дума → определение. Чети думи, докато не дойде „край“. За непозната дума отпечатай „Няма такава дума.“
- Оценки по предмет. Чети редове „Математика 6“, „Физика 5“, „Математика 4“ и отпечатай средния успех по всеки предмет, подредени по име.
Тест