LANG·I Язык Глава 10 из 65
Функции как значения
Кухня, где программу собирают из приёмов: нарезать каждый, отложить подходящие, смешать всё. Начинку — функцию — передают приёму как аргумент. Те же два приёма, разнесённые по тысячам машин, когда-то перемололи весь веб.
Язык
- 01 Первая программа
- 02 Переменные
- 03 Условия
- 04 Циклы
- 05 Функции
- 06 Списки
- 07 Строки
- 08 Словари
- 09 Рекурсия
- 10 Функции-значения вы здесь
- 11 Ошибки и тесты
- 12 Объекты
Опирается на: 09 · Задача внутри задачи 08 · Словарь и телеграф
Что вы унесёте из главы
- передавать функции в функции: map, filter, reduce, sorted и max с ключом
- писать короткие обработки данных включениями и генераторами, не держа всё в памяти
- понимать замыкания и декораторы — и как MapReduce разносит одну работу по тысячам машин
Прошлая глава кончилась вопросом: можно ли передать функции другую функцию? Четыре функции обхода вложенных списков — folder_size, show, nested_sum, flatten — различались только тем, что делать с простым элементом, а общий скелет не удавалось написать один раз: в функцию мы передавали числа, строки и списки, но не кусок поведения. Тот же повтор виден и в программах попроще, например в трёх циклах из прошлых глав.
Три цикла — и один скелет: завести пустой список, пройти по всем элементам, что-то сделать с каждым, положить результат. Различается только начинка: q[4], q[4] >= 7, len(word). Ради одной строчки мы каждый раз переписываем четыре. Повар так не работает. Кулинарная книга не объясняет заново, как резать кубиком, для моркови и отдельно для картошки: «нарезать кубиком» — приём, а что резать, подставляют.
Эта глава — кухня, на которой программы собирают из приёмов, а начинку передают им как аргумент. Для этого функцию придётся научиться класть в переменную, передавать и возвращать, как число.
Функция на разделочной доске
В главе 2 мы договорились, что имя в Python — ярлык на объекте. Число — объект, строка — объект, список — объект. Функция — тоже объект, а def всего лишь создаёт его и вешает на него ярлык с именем. Проверим.
Без скобок shout — сама функция, и с ней можно делать всё, что с любым значением: дать ещё одно имя, положить в список, передать другой функции. Со скобками shout("пожар") — вызов: функция работает, и на месте выражения оказывается её результат. На этом часто ошибаются: пишут print(shout) вместо вызова и получают загадочное <function shout at 0x7f…>. Так Python показывает саму функцию, с адресом в памяти.
Раз функция — значение, функции можно сложить в список и перебрать циклом, как ножи на магнитной планке:
Имя функции хранится в ней самой, в атрибуте __name__. Пять разных функций проходят через один и тот же вызов f(mags). А раз функцию можно положить в переменную, её можно получить и параметром. Напишем скелет из начала главы один раз, а начинку примем аргументом:
Во втором вызове передан str.upper — знакомый по главе 7 метод строк, взятый сам по себе, как функция одного аргумента: str.upper("уха") — то же, что "уха".upper(). Функцию, которая принимает другие функции или возвращает их, называют функцией высшего порядка. Наша apply_to_each — такая: она знает, как пройти по списку, но не знает, что делать с каждым элементом. Это ей сообщают при вызове.
Приём первый: нарезать каждый
Скелет «сделай с каждым» нужен так часто, что он встроен в Python и называется map — «отобразить»: каждому элементу сопоставить результат функции.
Первый print выдаёт вместо списка какой-то map object: map ещё ничего не посчитал. Он вернул обещание посчитать: заготовку, которая выдаёт результаты, когда их просят. list(…) попросил все сразу. Зачем такая лень, станет ясно ближе к концу главы, а пока будем оборачивать map в list.
Возьмём продукт посерьёзнее — каталог землетрясений из главы 6. Шкала магнитуд логарифмическая: по формуле Гутенберга и Рихтера энергия толчка растёт как $10^{1{,}5M}$, так что каждая единица магнитуды прибавляет энергии примерно в 32 раза. «Нарежем» каждую запись каталога в её энергию, в единицах «один толчок магнитуды 5»:
Здесь один map вложен в другой: внутренний превращает запись в магнитуду, внешний — магнитуду в энергию. Это композиция из главы 5, применённая сразу к целому потоку значений. Последняя строка вывода говорит, что один-единственный толчок у Камчатки 29 июля 2025 года по UTC выделил больше четверти энергии всех девятнадцати тысяч толчков магнитудой от пяти за одиннадцать лет. Формула грубая, и разные виды магнитуд в каталоге смешаны, но порядок величины она передаёт верно: сильные толчки редки и решают всё.
Рецепт на салфетке
Функция magnitude получила имя и две строки программы, хотя нужна была на одно мгновение — отдать её map. Для таких случаев есть короткая запись, лямбда:
Запись lambda q: q[4] читается так: «функция от q, которая возвращает q[4]». После слова lambda — параметры, после двоеточия — одно выражение, и его значение возвращается без всякого return. Имени у такой функции нет, внутри — только выражение: ни циклов, ни присваиваний. Это рецепт на салфетке: набросали прямо там, где нужен, и выбросили. Когда рецепт длиннее строки или нужен в двух местах, пишите обычный def — с именем и строкой документации.
Числа из одних функций
Как построить числа, если есть только функции? Чёрч предложил считать числом $n$ функцию, которая берёт действие f и повторяет его $n$ раз. Ноль — «ничего не делать», единица — «сделать один раз», два — «сделать, а потом ещё раз». Сложение и умножение тоже выражаются через функции.
Ни одного числа в определениях нет: числа появляются, только когда мы переводим их обратно функцией to_int. Из этой идеи выросло целое семейство языков — Лисп, Scheme, ML, Haskell. А в главе 51 мы напишем интерпретатор, внутри которого функции — такие же данные, как числа.
Приём второй: отложить подходящие
Второй приём кухни — перебрать и отложить годное: ягоды без плесени, грибы без червоточины. В Python этим занимается filter(условие, данные). Условие — функция, которая отвечает True или False; такие функции называют предикатами. Разрежем «Войну и мир» на слова и отложим перевёртыши из главы 7.
В функции read_words работает уже знакомый приём: map проходит по 2,9 миллиона символов романа и каждый не-буквенный заменяет пробелом. Лямбда в нём использует условное выражение a if условие else b: это однострочный if, который сам даёт значение, одно из двух. Потом filter откладывает палиндромы, set оставляет каждый по разу, а второй filter — только длинные. Заказ, казак, шабаш, топот — и даже «массам».
Приём третий: смешать всё
Третий приём превращает много в одно: смешать ингредиенты в тесто, сложить числа в сумму, выбрать из толчков сильнейший. Каждый раз мы держим в руках миску — накопитель из главы 4 — и подмешиваем в неё следующий элемент. Подмешивание — функция двух аргументов: что в миске и что добавить. Напишем скелет сами:
Нажмите «Шаги» и проследите третий вызов: миска проходит через 0, 1, 18, 184 и 1843 — цифры складываются в число, год публикации программы Ады Лавлейс. Такой приём называют свёрткой. В Python он живёт в модуле functools под именем reduce:
Без начального значения reduce кладёт в миску первый элемент и подмешивает остальные. Поэтому на пустом списке ему нечего положить, и он падает с TypeError. Если данные могут оказаться пустыми, передавайте начальное значение.
У reduce неоднозначная репутация. В 2005 году Гвидо ван Россум признался, что эту функцию всегда ненавидел больше прочих: почти каждый раз, видя reduce с нетривиальной функцией, он брал ручку и бумагу, чтобы нарисовать, что же в неё подаётся. С третьей версии Python reduce убрали из встроенных функций в functools. Для частых случаев есть готовые свёртки — sum, max, min, "".join, — а там, где готовой нет, цикл с накопителем часто читается легче. Но сама идея свёртки никуда не делась, и в конце главы она обработает весь веб.
Конвейер
Три приёма собираются в конвейер: выход одного — вход следующего. Ниже кухня, где его можно собрать руками. Добавляйте станции, меняйте начинку и смотрите, как продукты едут сверху вниз.
Попробуйте три опыта. На вкладке «числа» поменяйте местами «отложить» и «нарезать»: результат тот же — квадрат чётен ровно тогда, когда чётно само число, — а вызовов начинки больше, ведь квадраты считаются и для чисел, которые потом выбросят. Отбраковка до нарезки экономит работу. На вкладке «слова» выберите фильтр «длиннее десяти букв»: в миску не попадёт ничего, и вы получите ошибку пустого reduce из прошлого раздела. Наконец, поднимите «нарезать: len» выше фильтра, который меряет длину слов, — Python откажется: у числа нет длины. Начинка должна подходить к тому, что едет по ленте.
Ключ к сортировке
В главе 6 мы искали десять сильнейших толчков хитростью: перекладывали каждую запись в новый кортеж так, чтобы магнитуда стояла первой, — ведь кортежи сравниваются по первому элементу. Теперь можно проще. У sorted, max и min есть параметр key — функция, которая говорит, по чему сравнивать.
sorted вызывает ключ один раз для каждого элемента и упорядочивает элементы по ключам, а сами записи не меняет: они только встают в новом порядке. max с ключом ищет элемент с самым большим ключом и возвращает сам элемент. Ключом может быть любая функция: len, лямбда, ваша собственная. Если ключ — кортеж, сравнение идёт по первому элементу, при равенстве — по второму, и так далее. Чтобы одно поле шло по возрастанию, а другое по убыванию, у числа меняют знак.
Строки с одинаковым ключом не перемешиваются: сортировка в Python устойчивая, равные элементы сохраняют прежний порядок. На этом держится сортировка в несколько проходов. Включите в столе «сортировать то, что на столе», выберите сначала «магнитуда ↓», потом «страна» — и получите страны по алфавиту, а внутри каждой страны толчки от сильного к слабому. Тот же порядок даёт один ключ-кортеж: записи на столе устроены как (дата, магнитуда, глубина, страна), и ключ — (q[3], -q[1]). В каталоге из ячейки выше номера полей другие: магнитуда там q[4]. И ещё одна ловушка русского алфавита: буква «ё» стоит в таблице символов после «я», так что sorted(["ёрш", "щи"]) поставит ёрша последним.
Короткие рецепты: включения
«Нарезать каждый» и «отложить подходящие» встречаются вместе так часто, что для них в Python есть особая запись — включение, или списковое выражение. Одно такое вы уже видели в главе 8, на графике Ципфа: [45000 / r for r in ranks]. Квадратные скобки, внутри — что положить, откуда брать и, если нужно, какое условие пройти:
Запись повторяет математическое определение множества: $\{\,x^2 \mid x \in A,\ x > 2\,\}$ — «квадраты тех $x$ из $A$, что больше двух». Фигурные скобки с двоеточием дают словарь, без двоеточия — множество. Включение короче цикла и обычно понятнее, чем map с filter и лямбдой, поэтому в Python его любят больше. Но у него есть предел: включение с двумя циклами и тремя условиями уже не прочесть, и тогда лучше обычный цикл.
Есть задача для включений и в самой «Войне и мире». В третьем томе Пьер Безухов читает масонское толкование Апокалипсиса. Французские буквы получают числа: первые девять — от 1 до 9, остальные — десятки. Если записать так слова L'empereur Napoléon, сумма будет 666 — число зверя. Таблица напечатана прямо в романе. Кстати, это самое длинное «слово», которое наш read_words находит в книге: abcdefghiklmnopqrstuvwxyz. Проверим Пьера.
zip сшивает два списка в пары, как молния: первая буква с первым числом, вторая со второй. dict из таких пар — готовая таблица. А в number внутри sum(…) стоит включение без собственных скобок — о нём в разделе о генераторах.
«Сорок два» (quarante deux) дают 666, а у самого императора выходит 661. Счёт сходится, только если вернуть артиклю выпавшую букву: le empereur, ещё пять. Толстой, похоже, об этом знал. Через абзац Пьер ищет 666 в собственном имени, получает 671 и откидывает в артикле то самое «e» — «точно так же, хотя и неправильно». Программа поймала то, что роман сообщает намёком.
Закваска: функции, которые помнят
Функция может не только получить функцию, но и вернуть её. Вот фабрика, которая делает множители:
К моменту, когда вызывается double(10), функция make_scaler давно закончила работу, а её локальная переменная k, по правилам главы 5, должна была исчезнуть вместе с кадром. Но scale её помнит. Нажмите «Шаги»: при вызове double(10) в кадре scale рядом с x лежит k = 2, а при вызове triple(10) — k = 3. Функция унесла с собой кусочек того места, где родилась, как хлеб на закваске уносит кусочек прошлого теста. Функцию вместе с запомненными переменными называют замыканием.
Помнить можно и то, что меняется. В главе 5 мы видели, как счётчик падает с UnboundLocalError, если присвоить глобальной переменной внутри функции. У замыкания для этого есть слово nonlocal: «это имя из функции снаружи, своего не заводи».
У каждого счётчика своя закваска: a дошёл до трёх, b только начал. Глобальной переменной нет, и никто снаружи не может испортить счёт. Так замыкания прячут состояние, и в главе 12 мы увидим, что объекты делают то же самое другим способом.
Что напечатает print([f(10) for f in [lambda x: x * i for i in range(3)]]) — три лямбды, сделанные в цикле, применённые к 10?
Все три лямбды помнят не значение i, а саму переменную i, одну на всех. Когда их вызывают, цикл уже кончился, и i равно 2. Чтобы каждая лямбда запомнила своё значение, его передают параметром со значением по умолчанию: lambda x, i=i: x * i — значение по умолчанию вычисляется в момент создания функции.
По заказу: генераторы
Вернёмся к загадке map object. Хороший ресторан не варит сто порций с утра: он готовит по заказу. Пришёл заказ — приготовили одну тарелку, отдали, ждём следующего. В Python так работают генераторы. Это функции, в которых вместо return стоит yield — «отдай и подожди».
Вызов countdown(3) не напечатал «начинаю»: тело функции ещё не начало выполняться. Получился объект-генератор, повар у плиты. next(g) — заказ: генератор работает до первого yield, отдаёт значение и замирает на месте, со всеми своими переменными. Следующий next продолжает с той же строки. Цикл for заказывает раз за разом, пока повар не скажет «всё». Нажмите «Шаги»: кадр countdown появляется, исчезает и появляется снова, а n в нём каждый раз прежнее.
Толк от этого прежде всего в памяти. Список хранит все свои элементы сразу, генератор — только текущее состояние. Измерим пиковую память, пока считается сумма квадратов миллиона чисел. Функция peak принимает функцию — как приготовить данные:
Сумма одна и та же, а память отличается в десятки тысяч раз: около 40 мегабайт у списка и меньше килобайта у генератора. Вся разница — в скобках. Включение в круглых скобках — генераторное выражение: оно ничего не строит заранее и выдаёт элементы по одному. Внутри вызова функции вторые скобки можно не писать: sum(n * n for n in range(10)). Так и было в number у Пьера.
Второй толк — бесконечность. Бесконечный список не поместится ни в какую память, а бесконечный генератор занимает пару строк. Найдём квадраты, которые читаются одинаково в обе стороны, — заранее неизвестно, сколько чисел для этого перебрать:
islice из модуля itertools — срез для генераторов: «дай первые двенадцать». Цепочка работает задом наперёд. islice заказывает у filter одно значение, тот — у генератора квадратов, тот — у naturals. Число едет обратно по цепочке; не прошло фильтр — filter заказывает следующее. Ни на одном шаге не хранится больше одного числа, и цепочка останавливается, как только набралось двенадцать. Так устроены ленивые вычисления: ничего не делать, пока не попросили. map и filter устроены так же: они тоже выдают значения по одному и только по запросу.
Генератор отвечает и на стену из начала главы. Обход вложенных списков из главы 9 можно написать один раз — как генератор, который выдаёт простые элементы по одному, — а что с ними делать, решит тот, кто их получает:
Скелет обхода живёт в одном месте, а nested_sum из главы 9 превратилась в sum(leaves(data)), flatten — в list(leaves(data)). Двойной цикл в середине встречается так часто, что для него есть сокращение: yield from leaves(x).
Файл тоже читается лениво: цикл for line in open(…) берёт с диска строку за строкой, и роман в сотни мегабайт обрабатывается так же спокойно, как в пять. Цена ленивости одна: генератор одноразовый. Прошли по нему — он пуст. g = (n * n for n in range(5)), потом sum(g) даст 30, а второй sum(g) — ноль: повар уже ушёл домой. Если данные нужны дважды, сохраните их в список.
Обёртка
Осталось соединить два умения: функция принимает функцию — и возвращает новую. Новую можно сделать обёрткой вокруг старой, как кляр вокруг рыбы: внутри та же функция, снаружи — новый слой. Пусть обёртка считает вызовы. Наденем её на числа Фибоначчи из главы 9, у которых дерево вызовов полно повторов.
Строка @count_calls над определением — сокращение для fib = count_calls(fib): после def имя fib перевешивается на обёртку. Поэтому и рекурсивные вызовы внутри fib идут через обёртку, и счётчик видит все 242 785 вызовов ради одного числа 75 025. Функцию, которая принимает функцию и возвращает её улучшенную версию, называют декоратором. Счётчик здесь — атрибут самой функции-обёртки: функция — объект, и на неё можно повесить что угодно.
Декораторы пишут для того, что нужно многим функциям сразу: замерить время, записать вызов в журнал, проверить права, запомнить ответы. Чтобы обёртка годилась для функций с любым числом аргументов, её пишут как def wrapper(*args): звёздочка собирает все переданные аргументы в кортеж, а f(*args) раздаёт их обратно. А обёртка, которая запоминает уже посчитанные ответы и превращает 242 785 вызовов в 26 вычислений, лежит в стандартной библиотеке — functools.cache. Почему это работает и где ещё помогает, расскажет глава 22.
Кухня на тысячу машин
Классический пример из той статьи — подсчёт слов. Функция map получает кусок текста и на каждое слово выдаёт пару (слово, 1). Библиотека собирает все пары с одинаковым словом в одну кучку — это называют перетасовкой — и отдаёт каждую кучку функции reduce, которая складывает единицы. Ниже — восемь машин и знаменитый абзац о небе Аустерлица из первого тома «Войны и мира».
Опыты с машинами показывают две вещи из статьи. Первая: время всей работы — это время самой медленной машины. Одна машина с плохим диском тянет за собой тысячу; в статье описан случай, когда из-за ошибки в коде, настраивавшем машины при запуске, у части из них отключился кэш процессора и они работали в сотню раз медленнее. Резервные копии лечат это почти даром: на сортировке терабайта без них работа шла на 44 % дольше. Вторая: сломанную машину не нужно чинить, её кусок пересчитывают на другой. Когда в опыте авторы убили 200 рабочих процессов из 1746, весь расчёт занял на 5 % больше времени.
Перезапуск безопасен только потому, что map и reduce — чистые функции. Посчитать кусок дважды — всё равно что один раз: ответ зависит только от куска. Будь у map побочный эффект, например глобальный счётчик, повторный запуск посчитал бы слова дважды, и итог зависел бы от того, какие машины сломались. На это условие и ссылаются авторы: если пользовательские функции детерминированы, результат тот же, что у последовательного запуска на одной машине.
Тот же расчёт на всём романе, на «восьми машинах» внутри песочницы. Счётчик слов Counter вы знаете по главе 8; два счётчика складываются знаком +.
Здесь map и reduce в смысле Python, а не Google: каждая «машина» сразу считает слова своего куска, и сливаются готовые счётчики. В статье это тоже есть и называется комбинированием: оно сильно уменьшает перетасовку. Как устроить сам движок, в котором пары с одинаковым ключом находят друг друга, — последняя задача главы. Хозяин в нём один, MapReduce-мастер; как машинам договориться совсем без хозяина, расскажет глава 44.
Задачи: рецепты в вашу книгу
Пять задач, от разминки к самой трудной. В каждой тесты вызывают ваши функции и проверяют, что они вернули. Помните, что функция — значение: где-то вернуть нужно именно её, а не результат её вызова.
Толчки записаны кортежами (время, магнитуда, глубина, место), где место — строка вроде "48 km W of Illapel, Chile", а страна — кусок после последней запятой с пробелом (если запятой нет, страна — всё место). Напишите функцию by_country(quakes), которая возвращает новый список тех же кортежей: страны по алфавиту, внутри страны — от сильного толчка к слабому, а толчки с равной магнитудой в одной стране — в том порядке, в каком были во входном списке. Входной список не менять.
Сначала напишите отдельную функцию country(q): q[3].split(", ")[-1]. Если запятой нет, split вернёт список из одного куска — то, что нужно.
Ключ — кортеж из страны и магнитуды. Но reverse=True перевернёт и порядок стран. Как сделать, чтобы одно поле шло по возрастанию, а другое по убыванию?
Минус перед магнитудой: key=lambda q: (country(q), -q[1]). Равные ключи сортировка Python не переставляет — порядок входа сохранится сам.
Минус переворачивает только магнитуду, страны остаются по алфавиту. Устойчивость сортировки бесплатно даёт третье условие: при равных ключах сохраняется прежний порядок. То же можно получить двумя сортировками — сначала по магнитуде с reverse=True, потом по стране. Решение с reverse=True на кортеже (страна, магнитуда) не проходит: страны пошли бы от Z к A. Порядок равных толчков reverse при этом не ломает: устойчивость сохраняется и при обратной сортировке.
Напишите функцию compose(*funcs), которая принимает сколько угодно функций одного аргумента и возвращает их композицию — новую функцию, применяющую их справа налево, как в математике: compose(f, g, h)(x) — это f(g(h(x))). Без аргументов compose() возвращает функцию, которая отдаёт свой аргумент без изменений. Например, compose(len, str.strip)(" уха ") — это 3.
funcs внутри — кортеж функций. Начните с x и применяйте функции по очереди, начиная с последней: reversed(funcs) перебирает кортеж с конца.
Это свёртка: миска — текущее значение, подмешивание — применить к нему следующую функцию. Если функций нет, цикл не выполнится ни разу, и вернётся сам x.
composed — замыкание: оно помнит кортеж funcs, хотя compose давно вернулся. Ничего не вычисляется, пока не вызвали результат; одну и ту же композицию можно вызывать сколько угодно раз. Порядок «справа налево» — тот же, что у бус из главы 5, записанных на Python: inc(square(3)).
Напишите функцию make_tally(), которая возвращает функцию tally(word). Каждый вызов tally отвечает, в который раз она видит это слово: tally("мир") — 1, ещё раз tally("мир") — 2, tally("война") — 1. Разные вызовы make_tally() дают независимые счётчики. Глобальных переменных не заводите — тесты создают много счётчиков подряд.
В заготовке словарь seen один на всех. Где его завести, чтобы у каждого счётчика был свой?
Внутри make_tally, до определения tally. Словарь можно менять, не присваивая имени заново, поэтому nonlocal здесь даже не понадобится.
Каждый вызов make_tally создаёт новый словарь и новую функцию, которая его помнит. nonlocal нужен, только когда имя перевешивают на новый объект (count += 1 для числа); содержимое словаря можно менять и без него. Это та же разница между «изменить объект» и «перевесить ярлык», что в главе 6.
Напишите генератор primes(), который выдаёт простые числа по порядку без конца: 2, 3, 5, 7, 11, … Тесты возьмут первые десять, потом десятитысячное простое (оно равно 104 729), — на всё несколько секунд. Список «всех простых до N» не подойдёт: N заранее неизвестно.
Перебирайте числа с 2 бесконечным циклом while True и выдавайте простые через yield.
Проверять простоту делением на все числа до $\sqrt{n}$ можно, но быстрее делить только на уже найденные простые: храните их в списке. Остановиться можно, когда p * p > n.
takewhile из itertools лениво выдаёт элементы списка, пока условие верно, и останавливается на первом неверном: найденные простые идут по возрастанию, так что дальше корня он не пойдёт. all отвечает True, если все элементы истинны, и тоже останавливается на первом ложном — у составного числа на первом же делителе. Корень из 104 729 — около 323,6, а простых до 323 всего 66, так что на каждое число уходит не больше 66 делений. Генератор помнит список found между заказами, как замыкание помнит свои переменные.
Напишите движок mapreduce(chunks, mapper, reducer). chunks — список кусков данных. mapper(chunk) возвращает список пар (ключ, значение). Движок применяет mapper к каждому куску, собирает значения с одинаковым ключом в список (в порядке появления) и для каждого ключа вызывает reducer(ключ, значения). Вернуть нужно словарь ключ → результат reducer.
Затем напишите word_mapper(text) для подсчёта слов: слова — непрерывные последовательности букв (isalpha), в нижнем регистре; на каждое слово — пара (слово, 1). Тесты запустят ваш движок и с вашим word_mapper, и со своими функциями — для совсем других задач.
Три стадии — три цикла. Map: пройти по кускам и собрать все пары. Перетасовка: словарь groups, где каждому ключу соответствует список значений; groups.setdefault(key, []).append(value) заводит список при первой встрече. Reduce: словарь {key: reducer(key, values) for …}.
В word_mapper заготовка режет по пробелам, а слово «мир,» с запятой — не то же, что «мир». Замените всё, что не буква, пробелами, как в read_words, и приведите к нижнему регистру.
Движок ничего не знает ни о словах, ни о сложении. Тот же mapreduce с другими функциями находит сильнейший толчок каждой страны (map выдаёт пары (страна, магнитуда), reduce — max) или строит указатель «слово → номера страниц», с которого начинается любой поисковик, — его мы построим в главе 48. В гугловском MapReduce три цикла выполняются на разных машинах, а перетасовка идёт по сети, но договор с программистом тот же: дай две функции.
Куда дальше
Программы этой главы стали короче, но каждая строка теперь делает больше. В filter(lambda w: len(w) >= 5, found) легко перепутать знак, в ключе сортировки — забыть минус, в замыкании — поймать общую переменную цикла. Ни одна из этих ошибок не кричит: программа молча выдаёт неправильный ответ, как в главе 1. Чем длиннее программа, тем больше в ней таких мест и тем дороже ошибка.
4 июня 1996 года новая европейская ракета «Ариан-5» разрушилась примерно через 39 секунд после зажигания двигателя, в первом же полёте. Причиной оказался кусок программы, перенесённый с предыдущей ракеты, «Ариан-4», где он годами работал без единого сбоя. Как ловить ошибки до того, как они взорвут ракету, и что делать с теми, которые всё-таки случились, расскажет глава 11 — отчёт комиссии по расследованию.