Към съдържанието
wannacode

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.com
None
Потребители: 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.com
KeyError: 'nobody'

emails[ключ] не обхожда всички двойки, а намира мястото на ключа почти веднага, без значение колко са. Как го прави, е следващият урок.

Идеята

Складът: промени и проверки

Наличностите в склада: продукт → брой.

stock = {}
stock["лаптоп"] = 4
stock["мишка"] = 25
stock["лаптоп"] = 3
print(stock["лаптоп"])
print("монитор" in stock)
print(stock.get("монитор", 0))
del stock["мишка"]
print(len(stock))
Изход
3
False
0
1
  • Ключовете са уникални. Второто stock["лаптоп"] = 3 не добавя нов ред, а заменя 4 с 3.
  • ключ in stock пита дали ключът е вътре.
  • get(ключ, 0) връща стойността или 0, ако ключа го няма. Това пази от KeyError.
  • del stock[ключ] маха двойката.
По-точно, за любопитните
  • Стойностите могат да се повтарят: лаптоп и мишка може да са по 3. Ключовете не могат.
  • Може да питаш и 3 in stock.values(). Това обаче обхожда всички стойности, защото речникът е бърз само по ключ.

Пробвай

Колко са в менюто?

Какво ще отпечата програмата?

menu = {}
menu["кафе"] = 2
menu["чай"] = 2
menu["кафе"] = 3
print(len(menu), menu["кафе"])
Отговор

Идеята

Обхождане и редът

Цената на доставката по градове. items() дава двойките ключ и стойност, а for city, price in ги разделя на две имена:

delivery = {}
delivery["София"] = 4.5
delivery["Пловдив"] = 5.2
delivery["Варна"] = 6.0
delivery["Бургас"] = 6.0
delivery["Русе"] = 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.5
delivery["Пловдив"] = 5.2
delivery["Варна"] = 6.0
delivery["Бургас"] = 6.0
delivery["Русе"] = 5.8
print(delivery)
Изход
{'София': 4.5, 'Пловдив': 5.2, 'Варна': 6.0, 'Бургас': 6.0, 'Русе': 5.8}

Когато ти трябва азбучен ред, sorted(delivery) дава подреден списък с ключовете. Речникът не се променя:

delivery = {}
delivery["София"] = 4.5
delivery["Пловдив"] = 5.2
delivery["Варна"] = 6.0
delivery["Бургас"] = 6.0
delivery["Русе"] = 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) + 1
print(counts)
Изход
{'кафе': 3, 'чай': 2, 'вода': 1}

Това е най-честата употреба на речник: броене на думи, гласове, грешки по вид, продажби по град. Продуктите излизат в реда, в който са видени за първи път.

orders = "кафе чай кафе вода кафе чай"
counts = {}
for item in orders.split(" "):
counts[item] = counts.get(item, 0) + 1
print(counts)
Стъпка по стъпка, с думи
  1. 6 поръчки и празен речник.
  2. „кафе“ го няма: get(item, 0) дава 0, а присвояването добавя ключа с 1.
  3. „чай“ го няма: get(item, 0) дава 0, а присвояването добавя ключа с 1. Речникът пази реда на добавяне: новият ключ е последен.
  4. „кафе“ вече е вътре: get дава 1, а присвояването заменя стойността с 2.
  5. „вода“ го няма: get(item, 0) дава 0, а присвояването добавя ключа с 1. Речникът пази реда на добавяне: новият ключ е последен.
  6. „кафе“ вече е вътре: get дава 2, а присвояването заменя стойността с 3.
  7. „чай“ вече е вътре: get дава 1, а присвояването заменя стойността с 2.
  8. Всеки продукт е ключ веднъж, а стойността е колко пъти е поръчан.
По-точно, за любопитните
  • 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: 'кафе'

На кой ред е грешката?

Избери реда с грешката.

Идеята

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
Различни посетители: 3
True
  • set() е празно множество. {} не е: това е празен речник.
  • 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)

Упражнение

Речници в работа

Реши задачите в редактора си.

1. Таблица с потребители. Клас UserTable с методи add(username, email), get(username) и size(). Отвътре е речник. Добави ivanov, mpetrova и georgiev с имейли във вида ivanov@example.com и потърси mpetrova и nobody. В следващия урок ще направиш и своя хеш таблица, без dict.

Изход
mpetrova: mpetrova@example.com
nobody: няма такъв потребител
Потребители: 3

2. Честота на думите. Прочети ред текст, направи го с малки букви и преброй всяка дума. Отпечатай думите по азбучен ред.

Вход
Кафе с мляко и кафе без захар и кафе
Изход
без: 1
захар: 1
и: 2
кафе: 3
мляко: 1
с: 1

3. Документи по ключова дума. Клас 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“ и отпечатай средния успех по всеки предмет, подредени по име.

Тест

Провери се

1. Кога речник (dict) е по-добър избор от списък?
2. В речника menu вече има ключ "кафе" със стойност 2. Какво прави menu["кафе"] = 3?
3. Ключа "чай" го няма в menu. Какво правят menu["чай"] и menu.get("чай")?
4. Отпечатваш речник. В какъв ред излизат ключовете?
5. Какво връща add на set, ако стойността вече е вътре?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·