Практикум закрепляет модуль collections и global на восьми задачах. Примеры идут от самих инструментов (Counter, defaultdict, OrderedDict) к их применению в задачах практикума — так видно и общую механику, и то, как из неё складывается конкретное решение. Весь вывод в комментариях получен запуском кода.
Пример 1. Counter: подсчёт, топ, арифметика
from collections import Counter
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(counts.most_common(2)) # [('apple', 3), ('banana', 2)]
print(counts["apple"]) # 3
print(counts["missing"]) # 0 — отсутствующий ключ не бросает KeyError
more = Counter(["apple", "date"])
print(counts + more) # Counter({'apple': 4, 'banana': 2, 'cherry': 1, 'date': 1})
print(counts - more) # Counter({'apple': 2, 'banana': 2, 'cherry': 1})
print(list(counts.elements())[:5]) # ['apple', 'apple', 'apple', 'banana', 'banana']
Что происходит: Counter — это словарь «значение → количество», и обращение к отсутствующему ключу возвращает 0, а не ошибку, в отличие от обычного dict. +/- складывают и вычитают частоты (отрицательные результаты - отбрасывает), а elements() разворачивает счётчик обратно в поток значений — каждое повторяется по своей частоте.
Пример 2. Задание 1 — популярные слова из нескольких текстов
def popular_words(limit, *texts):
words = []
for text in texts:
words.extend(text.lower().replace(".", "").replace(",", "").split())
return Counter(words).most_common(limit)
text1 = "This is a sample text with some repeated words."
text2 = "Another sample text with different words."
text3 = "Text processing is fun when words repeat."
print(popular_words(5, text1, text2, text3))
# [('text', 3), ('words', 3), ('is', 2), ('sample', 2), ('with', 2)]
Что происходит: *texts принимает любое количество строк-аргументов; lower() нормализует регистр до подсчёта — без этого «Text» и «text» считались бы разными словами (ошибка №1 из топ-3). Точки и запятые убираются вручную replace, иначе к слову приклеился бы знак препинания.
Пример 3. Задание 2 — defaultdict(list): группировка задач по категории
from collections import defaultdict
def group_tasks(tasks):
result = defaultdict(list)
for task, category in tasks.items():
result[category].append(task)
return dict(result)
tasks = {"task1": "работа", "task2": "учёба", "task3": "развлечения", "task4": "работа", "task5": "учёба"}
grouped = group_tasks(tasks)
print(grouped)
# {'работа': ['task1', 'task4'], 'учёба': ['task2', 'task5'], 'развлечения': ['task3']}
Что происходит: без defaultdict пришлось бы на каждой итерации проверять if category not in result: result[category] = []. defaultdict(list) создаёт пустой список сам при первом обращении к новому ключу — result[category].append(task) работает сразу. В конце dict(result) убирает «фабрику по умолчанию» из вывода, чтобы результат выглядел как обычный словарь.
Пример 4. Задание 3 — поиск задач по категории
def find_tasks(tasks_by_category, category):
return tasks_by_category.get(category, [])
print(find_tasks(grouped, "учёба")) # ['task2', 'task5']
print(find_tasks(grouped, "спорт")) # [] — категории нет вовсе
Что происходит: результат группировки из примера 3 — обычный словарь «категория → список задач», поэтому поиск сводится к одному обращению. .get(category, []) возвращает пустой список для отсутствующей категории вместо KeyError — тот же принцип безопасного доступа, что и у Counter в примере 1, только явно через get, а не автоматически через defaultdict.
Пример 5. Задание 5 — defaultdict(int): счётчик посещений страниц
def count_visits(pages):
counts = defaultdict(int)
for page in pages:
counts[page] += 1
return dict(counts)
pages = ["home", "about", "home", "products", "home", "contact", "products"]
print(count_visits(pages))
# {'home': 3, 'about': 1, 'products': 2, 'contact': 1}
Что происходит: defaultdict(int) — тот же приём, что и defaultdict(list), но фабрика по умолчанию — int(), то есть 0. counts[page] += 1 для нового ключа сначала молча создаёт 0, потом прибавляет единицу — короче, чем ручной подсчёт через Counter из примера 1, и оправдан, когда нужен именно словарь с явным доступом по ключу, а не готовый most_common().
Пример 6. Задание 6 — группировка слов по длине
def group_by_length(words):
groups = defaultdict(list)
for word in words:
groups[len(word)].append(word)
return dict(groups)
fruit_words = ["apple", "banana", "kiwi", "grape", "orange", "peach"]
print(group_by_length(fruit_words))
# {5: ['apple', 'grape', 'peach'], 6: ['banana', 'orange'], 4: ['kiwi']}
Что происходит: ключом группировки здесь стало число (len(word)), а не строка — defaultdict одинаково работает с любым хешируемым ключом. Порядок ключей в выводе — порядок их первого появления в списке (5 раньше 6, потому что «apple» раньше «banana»), это общее свойство обычного dict в современном Python.
Пример 7. Задание 4 — OrderedDict: очередь по приоритету
from collections import OrderedDict
def reorder_by_priority(tasks_dict):
order = {"высокий": 0, "средний": 1, "низкий": 2}
for key, _ in sorted(tasks_dict.items(), key=lambda item: order[item[1]]):
tasks_dict.move_to_end(key)
return tasks_dict
tasks_od = OrderedDict({"task1": "низкий", "task2": "средний", "task3": "высокий", "task4": "низкий", "task5": "высокий"})
print(reorder_by_priority(tasks_od))
# OrderedDict([('task3', 'высокий'), ('task5', 'высокий'), ('task2', 'средний'), ('task1', 'низкий'), ('task4', 'низкий')])
print(list(tasks_od.keys()))
# ['task3', 'task5', 'task2', 'task1', 'task4']
Что происходит: задача просит изменить порядок исходного словаря, а не построить новый — поэтому sorted() только вычисляет нужную последовательность ключей, а переставляет их move_to_end(key), вызванный по очереди в нужном порядке. Пересоздание словаря (dict(sorted(...))) дало бы тот же порядок, но это уже другой объект — не то, что просит задание (ошибка №2 в топ-3).
Пример 8. Задание 7 — global и почему от него лучше уходить
counter = 0
def increment_counter():
global counter
counter += 1
increment_counter()
increment_counter()
print(f"Вызовов функции: {counter}")
# Вызовов функции: 2
Что происходит: без global counter строка counter += 1 внутри функции создала бы новую локальную переменную counter и упала бы с UnboundLocalError, потому что справа стоит чтение ещё не существующей локальной переменной. global явно говорит функции работать с переменной из модуля. Это ровно тот случай, где такая зависимость нужна редко: в большинстве проектов проще вернуть новое значение из функции и присвоить его снаружи — так функцию проще тестировать и переиспользовать.
Пример 9. Задание 8 — LRU-очередь на OrderedDict
def update_lru_queue(tasks, new_tasks, max_size):
queue = OrderedDict.fromkeys(tasks)
for task in new_tasks:
if task in queue:
queue.move_to_end(task)
else:
queue[task] = None
while len(queue) > max_size:
queue.popitem(last=False)
return list(queue)
result = update_lru_queue(
["task1", "task2", "task3", "task4", "task5", "task6"],
["task4", "task1", "task7", "task2"],
4,
)
print(result) # ['task4', 'task1', 'task7', 'task2']
Что происходит: LRU (least recently used) — правило вытеснения «выкидываем то, что дольше всего не трогали». Повторное обращение к задаче переносит её в конец через move_to_end (недавно использованные — «свежие»), новая задача добавляется в конец сразу, а popitem(last=False) удаляет элемент с начала — самый давний. Это тот же move_to_end, что и в примере 7, только применённый к другой задаче.
Пример 10. Когда всё же нужен именно OrderedDict, а не обычный dict
С Python 3.7 обычный dict тоже хранит порядок вставки — можно решить, что OrderedDict не нужен вовсе. Но у них разное поведение при сравнении.
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2) # True — обычный dict сравнивает только пары, порядок не важен
od1 = OrderedDict({"a": 1, "b": 2})
od2 = OrderedDict({"b": 2, "a": 1})
print(od1 == od2) # False — OrderedDict учитывает порядок при сравнении
print(list(d1.keys())) # ['a', 'b'] — порядок вставки сохранён и у обычного dict
Что происходит: оба типа сохраняют порядок вставки одинаково, но == у них разный: обычный dict считает {"a": 1, "b": 2} и {"b": 2, "a": 1} равными, а OrderedDict — нет. Значит, OrderedDict стоит выбирать не ради порядка как такового (его даёт и обычный dict), а ради методов вроде move_to_end() и popitem(last=False/True) — то, чего у обычного словаря нет и что использовано в примерах 7 и 9.
OrderedDict — деталь, выходящая за рамки практикума; см. collections.OrderedDict.
Что дальше
Все восемь задач практикума — в заданиях, разбор решений — в решениях. Частые промахи с этими структурами данных — в типичных ошибках.