Данные и итерация — Примеры
Пример 1 — Скан, спрятанный внутри цикла
Ночная задача сопоставляет пользователей со списком подписчиков и на входах по нескольку тысяч с каждой стороны идёт минутами, причём обе стороны приходят обычными списками. Перепишите поиск так, чтобы стоимость больше не росла с обеими длинами.
subscribers = read_subscribers() # список словарей
allowed = []
for user in read_users():
for sub in subscribers:
if sub["email"] == user["email"]:
allowed.append(user)
Решение
subscribers = read_subscribers()
by_email = {sub["email"] for sub in subscribers}
allowed = [user for user in read_users() if user["email"] in by_email]
Проверка вхождения в множестве занимает фактически константное время, поэтому каждый пользователь получает ответ за одну проверку вместо прохода по всему списку подписчиков, а сами пользователи по-прежнему читаются за один проход. Ошибка здесь — оставить вложенный цикл и подкручивать его; однако стоимость решает коллекция, а не цикл. Когда нужен ответ-значение, а не «да или нет», берите словарь.
Пример 2 — Обработка файла, который не влезает в память
Скрипт суммирует числа в файле выгрузки: на образце он работает, а на настоящем файле его убивают, потому что тот в несколько раз больше доступной памяти. Перепишите его так, чтобы в памяти держалась одна строка.
def totals(path):
with open(path) as handle:
numbers = [int(line) for line in handle]
return sum(numbers)
Решение
def lines(path):
with open(path) as handle:
for line in handle:
yield line.strip()
def totals(path):
return sum(int(line) for line in lines(path) if line)
Каждое значение выдаётся тогда, когда его запрашивают, поэтому конвейер держит только текущую строку и работает с файлом больше памяти, а сам файл всё равно закрывается, когда генератор исчерпан. Ошибка — вернуть list(...) где-то в середине конвейера: это кладёт весь файл обратно в память и возвращает исходный сбой. Генератор выручает и тогда, когда нужен только первый подходящий элемент, а остальные были бы отброшены.