Примеры идут от простого к сложному: измерение времени, затем каждая структура модуля collections по отдельности — OrderedDict, defaultdict, Counter — с типичными применениями и подводными камнями, и в конце — functools.lru_cache и связка двух инструментов вместе. Весь вывод в комментариях получен запуском кода.
Пример 1. Измерение времени выполнения кода
time.time() возвращает текущее время в секундах с начала эпохи Unix; разность двух вызовов даёт грубую оценку длительности. Для коротких участков кода точнее time.perf_counter() — он не зависит от системных часов.
# measure_time.py
import time
start = time.time()
values = list(range(1000))
end = time.time()
print(len(values)) # 1000
print((end - start) >= 0) # True
t0 = time.perf_counter()
total = sum(range(100000))
t1 = time.perf_counter()
print(total) # 4999950000
print((t1 - t0) >= 0) # True
time.perf_counter() не упомянут в конспекте урока (там только time.time() и sleep()), но это рекомендуемый инструмент для замеров в реальном коде. Сверьтесь с разделом time.perf_counter().
Пример 2. OrderedDict: порядок и move_to_end()
OrderedDict — словарь с дополнительными операциями управления порядком пар: можно переместить любой ключ в начало или конец.
# ordered_dict_basics.py
from collections import OrderedDict
queue = OrderedDict()
queue["first"] = 1
queue["second"] = 2
queue["third"] = 3
print(queue) # OrderedDict([('first', 1), ('second', 2), ('third', 3)])
print(queue.popitem(last=False)) # ('first', 1) — удалили самый первый
print(queue) # OrderedDict([('second', 2), ('third', 3)])
od = OrderedDict({"a": 1, "b": 2, "c": 3})
od.move_to_end("a")
print(od) # OrderedDict([('b', 2), ('c', 3), ('a', 1)])
od.move_to_end("c", last=False)
print(od) # OrderedDict([('c', 3), ('b', 2), ('a', 1)])
Что происходит: popitem(last=False) удаляет первый добавленный элемент — это делает OrderedDict удобным для очереди FIFO. move_to_end(key, last=False) перемещает ключ в начало, а не в конец — параметр называется одинаково у обоих методов, но управляет разными сторонами структуры.
Пример 3. Обычный dict тоже хранит порядок вставки
В современном Python обычный dict с версии 3.7 гарантированно сохраняет порядок добавления ключей — это стандарт языка, а не побочный эффект реализации. Разница с OrderedDict в другом: сравнение на равенство.
# dict_order_vs_ordereddict.py
from collections import OrderedDict
d1 = {"first": 1, "second": 2}
d2 = {"second": 2, "first": 1}
print(d1 == d2) # True — обычный dict сравнивает без учёта порядка
print(list(d1) == list(d2)) # False — порядок ключей разный
print(OrderedDict(d1) == OrderedDict(d2)) # False — OrderedDict сравнивает и порядок тоже
Что происходит: в теории OrderedDict подан как «нужен для порядка», но это устарело — порядок сохраняет и обычный dict. Настоящая причина использовать OrderedDict сегодня — методы move_to_end() и popitem(last=False), которых у обычного dict нет, и то, что == для него учитывает порядок.
Пример 4. functools.lru_cache: кэш вызовов функции
@lru_cache(maxsize=N) запоминает результат функции по значениям аргументов. Повторный вызов с теми же аргументами не выполняет тело функции заново, а сразу возвращает сохранённый результат.
# lru_cache_basics.py
from functools import lru_cache
@lru_cache(maxsize=2)
def compute_square(number):
print(f"compute {number}")
return number * number
print(compute_square(2)) # compute 2 \n 4
print(compute_square(2)) # 4 — без "compute 2", результат взят из кэша
print(compute_square(3)) # compute 3 \n 9
print(compute_square.cache_info())
# CacheInfo(hits=1, misses=2, maxsize=2, currsize=2)
print(compute_square(4)) # compute 4 \n 16 — вытеснил самый старый результат (2)
print(compute_square.cache_info())
# CacheInfo(hits=1, misses=3, maxsize=2, currsize=2)
Что происходит: при втором вызове compute_square(2) строка "compute 2" не печатается — функция вообще не выполнялась. maxsize=2 ограничивает кэш двумя записями: когда приходит третий уникальный аргумент, самая старая запись вытесняется по принципу LRU (Least Recently Used).
Пример 5. defaultdict: группировка и подсчёт без проверок
defaultdict(factory) вызывает factory() и создаёт значение автоматически, если обращаются к отсутствующему ключу. Для группировки берут list, для подсчёта — int.
# defaultdict_grouping.py
from collections import defaultdict
students = [("Иван", "Физика"), ("Мария", "Математика"), ("Пётр", "Физика")]
result = defaultdict(list)
for name, faculty in students:
result[faculty].append(name)
print(dict(result))
# {'Физика': ['Иван', 'Пётр'], 'Математика': ['Мария']}
counts = defaultdict(int)
for _, faculty in students:
counts[faculty] += 1
print(dict(counts))
# {'Физика': 2, 'Математика': 1}
Что происходит: без defaultdict пришлось бы писать if faculty not in result: result[faculty] = [] перед каждым append(). counts[faculty] += 1 работает даже для первого упоминания факультета: defaultdict(int) подставляет 0, и 0 + 1 даёт 1.
Пример 6. Ловушка: defaultdict создаёт ключ уже при чтении
Обращение по [] к отсутствующему ключу — это не только чтение, но и запись: defaultdict сразу добавляет ключ со значением фабрики. Если нужно только проверить значение, не изменяя словарь, — используйте get().
# defaultdict_mutates_on_read.py
from collections import defaultdict
dd = defaultdict(list)
print(dd.get("missing", [])) # []
print(dict(dd)) # {} — get() не создал ключ
print(dd["missing"]) # []
print(dict(dd)) # {'missing': []} — а [] создал!
.get(). Если код собирается тут же дополнить значение (append(), += 1) — используйте [], ради этого defaultdict и существует.
Пример 7. Counter: частотный анализ текста
Counter строит словарь частот из любой последовательности за один вызов. most_common(n) возвращает n самых частых элементов, отсортированных по убыванию количества.
# counter_word_frequency.py
from collections import Counter
text = "This is a test. This test is only a test."
words = text.lower().replace(".", "").replace(",", "").split()
word_count = Counter(words)
print(dict(word_count))
# {'this': 2, 'is': 2, 'a': 2, 'test': 3, 'only': 1}
print(word_count.most_common(2))
# [('test', 3), ('this', 2)]
Что происходит: перед подсчётом текст приводится к нижнему регистру и очищается от точек — иначе "This" и "this." считались бы разными словами. most_common(2) берёт два самых частых слова; при равном количестве порядок среди них определяется порядком первого появления.
Пример 8. Counter: update(), subtract(), elements() и арифметика
Counter поддерживает операторы + и -, а также методы update() (добавить счётчики) и subtract() (вычесть). Разница между - и subtract() — в отрицательных значениях.
# counter_arithmetic.py
from collections import Counter
c1 = Counter("banana")
print(c1) # Counter({'a': 3, 'n': 2, 'b': 1})
c1.update("nan")
print(c1) # Counter({'a': 4, 'n': 4, 'b': 1})
c2 = Counter(a=3, b=1)
c3 = Counter(a=1, b=2)
print(c2 + c3) # Counter({'a': 4, 'b': 3})
print(c2 - c3) # Counter({'a': 2}) — b получился бы -1, оператор его отбросил
print(list((c2 - c3).elements())) # ['a', 'a']
c2.subtract(c3)
print(c2) # Counter({'a': 2, 'b': -1}) — subtract() отрицательные значения сохраняет
Что происходит: оператор - создаёт новый Counter и молча убирает записи с нулевым или отрицательным количеством — это удобно для «что осталось после вычитания». subtract() меняет счётчик на месте и сохраняет отрицательные значения — подходит, когда отрицательное число само по себе значимо (например, «на сколько не хватает»).
Пример 9. Очередь задач по приоритету на OrderedDict
Практическая задача из лекции: расставить задачи по приоритету, перемещая записи OrderedDict внутри одного прохода по items().
# priority_queue.py
from collections import OrderedDict
tasks = OrderedDict({
"task1": "low priority",
"task2": "medium priority",
"task3": "low priority",
"task4": "high priority",
})
for key, value in list(tasks.items()):
if "low" in value:
tasks.move_to_end(key)
if "high" in value:
tasks.move_to_end(key, last=False)
print(tasks)
# OrderedDict([('task4', 'high priority'), ('task2', 'medium priority'),
# ('task1', 'low priority'), ('task3', 'low priority')])
Что происходит: цикл идёт по list(tasks.items()) — копии пар на момент старта, а не по самому словарю, который меняется внутри цикла. Так безопасно переставлять элементы во время обхода: без копии Python выбросил бы RuntimeError: dictionary changed size during iteration (здесь размер не меняется, только порядок, но копия — общий безопасный приём).
Пример 10. Ограничения на практике: нехешируемый Counter и фабрика-функция для defaultdict
Два дополнения, которых нет в лекции напрямую, но которые часто встречаются: почему Counter не примет список элементов-списков, и что фабрикой defaultdict может быть любая функция, а не только тип вроде list/int.
# practical_limits.py
from collections import Counter, defaultdict
try:
bad_counter = Counter([["a"], ["b"]])
except TypeError as error:
print("TypeError:", error)
# unhashable type: 'list'
def default_score():
return {"wins": 0, "losses": 0}
scoreboard = defaultdict(default_score)
scoreboard["alice"]["wins"] += 1
scoreboard["bob"]["losses"] += 1
scoreboard["alice"]["wins"] += 1
print(dict(scoreboard))
# {'alice': {'wins': 2, 'losses': 0}, 'bob': {'wins': 0, 'losses': 1}}
Что происходит: Counter считает элементы, а элементы становятся ключами внутреннего словаря — список внутри списка нехешируем, поэтому подсчёт падает. Во втором блоке defaultdict(default_score) вызывает свою функцию при каждом новом ключе — так получают словарь словарей со своей структурой по умолчанию, а не просто list или int.
Что делать дальше
Посмотрите файлы репозитория — там те же идеи в исходном виде из лекции, затем переходите к заданиям. Если что-то не запустилось — загляните в типичные ошибки.