четверг, 3 февраля 2011 г.

Переходим на Python 3. Где же ты, reduce?

Это мой второй пост об освоении Python 3. Начался он с того, что захотелось мне использовать всем известную встроенную функцию reduce, а я вместо рабочего кода получил NameError. Оказывается в Python 3 она уже не встроенная, а находится в module functools, в который, начиная с версии Python 2.5, всунули несолько полезностей для работы с объектами-функциями. То есть теперь функцию reduce нужно импортировать.
from functools import reduce
Стоит заметить, что спецификация функции не поменялась, работает она точно также как и во втором питоне. Постал вопрос: "Зачем?". (Более подробно о reduce читаем в документации).

С чего все началось?
А началось все с Гвидо ван Россума, сделавшего следующее высказывание, когда Python 3k только начинали делать. Вот вольный перевод:
Около 12 лет назад в Python появились lambda, reduce(), filter() и map(); появились они с соизволения (мне кажется) Lisp-хакера, которому не хватало их в Python, и который предоставил работающие патчи. Но, невзирая ни на что, я думаю, что эти вещи нужно вырезать из Python 3000.
Также известно, что Гвидо считает эти вещи ненужными, так как есть так называемые "list comprehensions", то есть конструкции типа:
>>> [i * 2 for i in my_list if i > 0]
Вот мнение "великодушного диктатора" о reduce:
Теперь о reduce(). На самом деле это то, что я ненавижу больше всего, потому что кроме нескольких примеров с + или *, почти всегда, когда я вижу вызов reduce() с нетривиальной функцией, мне нужно брать ручку и бумагу, чтобы нарисовать диаграму того, что же действительно передается в функцию перед тем, как понимаю, для чего на самом деле здесь использовалась reduce(). Так что, по-моему, reduce() - практически ограничена ассоциативными операторами, и во всех других случаях лучше сделать явный кумулятивный цикл.
Нужна ли reduce вообще?
Просмотрев свой код и вижу, что за 3 с лишним года работы с Python я использовал reduce, в отличие от map, filter и lambda, очень редко. Задумываясь о различных способах реализации того или иного блока кода, можно увидеть массу случаев, где нужно применить reduce и в большинстве из них находятся альтернативные решения, которые делают код более понятным и читабельным. Рассмотрим несколько примеров на простом списке:
>>> v = [0,1,2,3,4]
Суммирование:
>>> r = reduce(lambda x, y: x + y, v)
>>> print(r)
10
Ествественно, такое никому не нужно, когда есть sum:
>>> sum(v)
>>> 10

Рассмотрим умножение:
>>> v = [1, 2, 3, 4]

>>> reduce(lambda x, y: x * y, v)
>>> 24

>>> r = 1
>>> for i in v:
>>> r *= i
>>> 24
Здесь вариант с reduce выглядит более чем привлекательным.

Соединение списков:
>>> reduce(list.__add__, [[1, 2, 3], [4, 5], [6, 7, 8]], [])
[1, 2, 3, 4, 5, 6, 7, 8]

>>> from itertools import chain
>>> list(chain([1, 2, 3], [4, 5], [6, 7, 8]))
[1, 2, 3, 4, 5, 6, 7, 8]
По-моему, вариант с itertools является более понятным и, что гораздо интереснее, возвращает не список, а ... догадайтесь сами. Для других задач зачастую находятся более красивые, или более читабельные решения, например, для логических - использование функций any и all.

reduce для меня и заключение
Иногда с использованием reduce замечаю, что через некоторое время код воспринимается тяжелее, особенно, если это не тривиальное использование оператора, а reduce помешанная с множественными вложенными вызовами функций, поэтому стараюсь применять reduce там, где это действительно упрощает код и делает его красивее, а такие ситуации, по-моему, возникают очень редко и должны быть видны сразу: если уже задумываешся, значит что-то не так, что-то некрасиво.

Вообще-то я поддерживаю решение вынести reduce из разряда встроенных функций в модуль functools, теперь, перед тем как нагадить в коде дважды подумаю, ну для этого ж нужно еще один дополнительный импорт! :)

Всем спасибо за внимание, кастую холиварщиков в комменты...

10 комментариев:

  1. Isem, спасибо за поправку, наверное не из того терминала скопипастил =(.
    Слово-то какое ... терминал .. в винде =)

    ОтветитьУдалить
  2. Раз теперь исправлено, надо и мой комментарий исправить на:
    Почему 1*2*3*4 = 24 ?
    :)

    ОтветитьУдалить
  3. Я думаю, что вариант с
    product( v ) будет выглядеть еще более привлекательным.

    ОтветитьУдалить
  4. Естественно будет, только чтоб не засорять пост кучей кода, хотел показать в трех примерах как можно больше вариантов написания. Поэтому выбрал такой. Идея была в том, чтобы понять ход мыслей разработчиков при тех или иных изменениях в Python 3 по сравнению с Python 2. Специфика стороны рассмотрения вопроса "Переходим на Python 3", а не "Изучаем Python 3". Как-то так...

    ОтветитьУдалить
  5. Да, согласен. Но единственный (сильно сказано) вариант, где reduce будет приемлемым, на мой взгляд, это когда бинарная функция заранее неизвестна. И reduce - это еще тот случай, когда читабельность граничит с эффективностью (выполнения). В конце концов, reduce есть, пусть даже для этого надо добавить еще одну строчку вначале, и выбор, как всегда, остается за программистом.

    ОтветитьУдалить
  6. Я редьюсом список в строку склеиваю.
    reduce(str.__add__, map(lambda x: ("0", x)[int(x in allowed)], value), "")
    Вот так, например.
    Подозреваю, что есть какой-то более прямой способ это сделать (кроме for).

    ОтветитьУдалить
    Ответы
    1. Так и не понял, как работает этот способ склеивания. Это такая шутка?

      А product из itertools считает декартово произведение, так что факториал тут не при чем.

      Удалить
    2. Чего тут непонятного? Склеиваем все элементы в повторяемом (iterable) value, также содержащиеся в allowed (для которого определён __contains__). Если элемент не содержится в allowed, на его место ставим LATIN SMALL LETTER O. Чем человеку не угодил join и тернарный оператор (либо псевдо‐тернарный‐оператор «a and b or c» при условии, что все элементы в allowed истинны) непонятно: более прямой эквивалент будет

      ''.join((x if x in allowed else 'o' for x in value))

      Удалить
  7. Кстати, для соединения листов можно применить sum([[1, 2, 3], [4, 5], [6, 7, 8]],[]). Работает и в 2.7.6, что ещё раз указывает на бессмысленность reduce в этом случае. XD

    ОтветитьУдалить

В этом гаджете обнаружена ошибка