Пример 1. Измерение времени
Сравним скорость создания объекта range и списка.
import time
start = time.time()
values = range(1_000_000)
elapsed_range = time.time() - start
start = time.time()
values_list = list(range(1_000_000))
elapsed_list = time.time() - start
print(f"range: создаётся мгновенно (лениво)")
print(f"list: требует заполнить память, elapsed_list > 0: {elapsed_list > 0}")
# range: создаётся мгновенно (лениво)
# list: требует заполнить память, elapsed_list > 0: TrueЧто происходит: range хранит только начало, конец и шаг — числа вычисляются по требованию (ленивая последовательность). list(range(...)), наоборот, сразу создаёт миллион объектов в памяти, поэтому измеримо медленнее.
Пример 2. OrderedDict как очередь задач
OrderedDict — словарь, который явно хранит порядок вставки и добавляет методы popitem(last=False) и move_to_end().
from collections import OrderedDict
od = OrderedDict()
od["a"] = 1
od["b"] = 2
od["c"] = 3
print(od.popitem(last=False)) # первый вставленный элемент
print(od)
# ('a', 1)
# OrderedDict([('b', 2), ('c', 3)])
queue = OrderedDict([
("task1", "low"),
("task2", "high"),
("task3", "low"),
])
for key, value in list(queue.items()):
if "high" in value:
queue.move_to_end(key, last=False)
elif "low" in value:
queue.move_to_end(key)
print(queue)
# OrderedDict([('task2', 'high'), ('task1', 'low'), ('task3', 'low')])Почему так: задачи с приоритетом high перемещаются в начало (last=False), задачи с low — в конец. Обычный dict тоже сохраняет порядок вставки с Python 3.7+, но не даёт этих специализированных методов.
Пример 3. LRU-кэш на практике
Декоратор @lru_cache запоминает результаты вызовов и ускоряет рекурсивное вычисление чисел Фибоначчи — без него fibonacci(35) считался бы экспоненциально долго.
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35))
print(fibonacci.cache_info())
# 9227465
# CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)Что происходит: каждое новое значение n считается один раз и кладётся в кэш (misses); при повторном обращении к уже посчитанному n результат берётся из кэша (hits) — отсюда мгновенный ответ даже для n = 35.
Пример 4. frozenset как ключ словаря
Используем неизменяемое множество там, где обычный set не подходит — например, ключом словаря.
frozen = frozenset(["read", "write"])
permissions = {frozen: "editor"}
print(permissions[frozen])
# editorПример 5. Counter: подсчёт частоты элементов
Counter — специализированный словарь для подсчёта: считает, сколько раз встретился каждый элемент, и умеет отдавать самые частые.
from collections import Counter
votes = ["red", "blue", "red", "green", "blue", "red"]
tally = Counter(votes)
print(tally)
print(tally.most_common(2))
# Counter({'red': 3, 'blue': 2, 'green': 1})
# [('red', 3), ('blue', 2)]Counter назван в кратком описании занятия, но отдельно на занятии не разбирался — добавлен как важное дополнение к теме collections.Пример 6. deque: быстрое добавление и удаление с обоих концов
deque (double-ended queue) — как список, но добавление/удаление с обоих концов выполняется за O(1); с параметром maxlen старые элементы автоматически вытесняются.
from collections import deque
recent_events = deque(maxlen=3)
for event in ["login", "click", "scroll", "logout"]:
recent_events.append(event)
print(recent_events)
# deque(['click', 'scroll', 'logout'], maxlen=3)
history = deque([1, 2, 3])
history.appendleft(0)
history.append(4)
print(history)
# deque([0, 1, 2, 3, 4])Что происходит: у recent_events задан maxlen=3 — при добавлении четвёртого элемента ("logout") самый старый ("login") автоматически удаляется. У обычного списка такого поведения нет.
deque назван в кратком описании занятия, но отдельно на занятии не разбирался — добавлен как важное дополнение к теме collections.Пример 7. defaultdict: группировка без ручной проверки ключа
defaultdict создаёт значение по умолчанию при первом обращении к отсутствующему ключу — не нужно писать if key not in dict: dict[key] = [].
from collections import defaultdict
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
print(dict(by_letter))
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}defaultdict назван в кратком описании занятия, но отдельно на занятии не разбирался — добавлен как важное дополнение к теме collections.Пример 8. Основные операции со словарём
Добавление, обновление, проверка ключа и удаление.
profile = {"name": "Alice", "age": 30}
profile["city"] = "Berlin"
profile.update({"age": 31, "email": "alice@example.com"})
if "email" in profile:
print(profile["email"])
removed = profile.pop("email")
print(f"Удалено: {removed}")
print(profile)
# alice@example.com
# Удалено: alice@example.com
# {'name': 'Alice', 'age': 31, 'city': 'Berlin'}Пример 9. Передача функции в функцию
Функцию можно передать как аргумент и вызвать внутри обёртки — функции в Python такие же объекты, как числа и строки.
def double(x):
return x * 2
def apply(func, data):
return [func(x) for x in data]
print(apply(double, [1, 2, 3]))
# [2, 4, 6]Пример 10. Lambda с сортировкой
Сортируем список кортежей по нескольким критериям сразу: сначала по убыванию возраста, затем по имени.
people = [("Mike", 19), ("Nancy", 35), ("Oscar", 33), ("Frank", 33)]
by_age = sorted(people, key=lambda p: p[1])
by_age_name = sorted(people, key=lambda p: (-p[1], p[0]))
print(by_age)
print(by_age_name)
# [('Mike', 19), ('Oscar', 33), ('Frank', 33), ('Nancy', 35)]
# [('Nancy', 35), ('Frank', 33), ('Oscar', 33), ('Mike', 19)]Пример 11. Парадигмы программирования: функциональный подход
Python — мультипарадигменный язык: императивный, процедурный, ООП, функциональный, декларативный стили. Функциональный подход строит новый объект вместо изменения старого.
# функциональный подход: новый список вместо изменения старого
numbers = [1, 2, 3]
doubled = [x * 2 for x in numbers]
print(doubled)
# [2, 4, 6]Пример 12. Ловушка: забытый list() и попытка изменить frozenset
Две частые ошибки: печатать сам объект-итератор вместо результата, и пытаться изменить неизменяемую коллекцию.
numbers = [1, 2, 3]
doubled_iterator = map(lambda x: x * 2, numbers)
print(doubled_iterator) # <map object at ...> — это не список
print(list(doubled_iterator)) # только так виден результат
# <map object at 0x...>
# [2, 4, 6]
frozen = frozenset([1, 2, 3])
try:
frozen.add(4)
except AttributeError as error:
print(f"Ошибка: {error}")
# Ошибка: 'frozenset' object has no attribute 'add'
try:
wrong_key = {1, 2}
lookup = {wrong_key: "value"}
except TypeError as error:
print(f"Ошибка: {error}")
# Ошибка: unhashable type: 'set'Почему так: map()/filter() возвращают ленивые итераторы, а не готовые списки — печать самого объекта показывает его тип и адрес в памяти, а не значения. frozenset неизменяем — у него нет add(). Обычный set изменяем, поэтому нехешируем и не может быть ключом словаря (в отличие от frozenset из примера 4).