Зачем программировать функционально в Python?

На работе мы использовали программу Python в довольно стандартном способе OO. В последнее время пара парней попала в функциональную подножку. И их код теперь содержит много больше лямбда, карты и уменьшает. Я понимаю, что функциональные языки хороши для concurrency, но действительно ли программирование Python действительно помогает с помощью concurrency? Я просто пытаюсь понять, что получаю, если я начну использовать больше функциональных возможностей Python.

Ответ 1

Изменить. Я был привлечен к задаче в комментариях (частично, по-видимому, фанатиками FP на Python, но не исключительно), чтобы не давать больше объяснений/примеров, поэтому, расширение ответа на поставку некоторых.

lambda, тем более mapfilter), и, в особенности reduce, вряд ли будут правильным инструментом для работы на Python, что сильно язык мультипарадигмы.

lambda основное преимущество (?) по сравнению с обычным выражением def заключается в том, что он выполняет функцию анонимного, а def дает функции имя - и для этого очень сомнительный преимущество в том, что вы платите огромную цену (тело функции ограничено одним выражением, результирующий объект функции не разборчив, сам недостаток имени иногда затрудняет понимание трассировки стека или отлаживает проблему - мне нужно идти на -!).

Считайте, что, вероятно, самая идиотичная идиома, которую вы иногда видите в "Python" (Python с "кавычками", потому что это явно не идиоматический Python - это плохая транслитерация из идиоматической схемы или аналогично тому, как более частое злоупотребление ООП в Python - плохая транслитерация с Java и т.п.):

inc = lambda x: x + 1

назначив лямбда имени, этот подход сразу же выбрасывает вышеупомянутое "преимущество" и не теряет никаких недостатков! Например, inc не знает его имя - inc.__name__ - бесполезная строка '<lambda>' - удача в понимании трассировки стека несколькими из них;-). Разумеется, правильный способ Python для достижения желаемой семантики в этом простом случае:

def inc(x): return x + 1

Теперь inc.__name__ - это строка 'inc', как это должно быть ясно, и объект является макробливым - семантика в остальном идентична (в этом простом случае, когда желаемая функциональность удобно помещается в простом выражении - def также делает его тривиально простым для рефакторинга, если вам нужно временно или постоянно вставлять такие выражения, как print или raise, конечно).

lambda является (частью) выражения, в то время как def является (частью) инструкции - что один бит синтаксического сахара, который заставляет людей использовать lambda иногда. Многие энтузиасты FP (как и многие ООП и процедурные фанаты) не любят Python достаточно сильное различие между выражениями и высказываниями (часть общей позиции по отношению к Command-Query Separation). Я думаю, что, когда вы используете язык, вам лучше использовать его "с зерном" - способ использования , а не бороться с ним; поэтому я программирую Python в Pythonic, Scheme in the Schematic (;-) way, Fortran в стиле Fortesque (?) и т.д.: -).

Переходя к reduce - один комментарий утверждает, что reduce - лучший способ вычислить произведение списка. Да неужели? Давайте посмотрим...:

$ python -mtimeit -s'L=range(12,52)' 'reduce(lambda x,y: x*y, L, 1)'
100000 loops, best of 3: 18.3 usec per loop
$ python -mtimeit -s'L=range(12,52)' 'p=1' 'for x in L: p*=x'
100000 loops, best of 3: 10.5 usec per loop

поэтому простой, элементарный, тривиальный цикл примерно в два раза быстрее (а также более краткий), чем "лучший способ" выполнить задачу?). Я полагаю, что преимущества скорости и лаконичности должны поэтому делать тривиальную петлю "самый лучший" способ, верно? -)

Путем дальнейшего жертвования компактностью и удобочитаемостью...:

$ python -mtimeit -s'import operator; L=range(12,52)' 'reduce(operator.mul, L, 1)'
100000 loops, best of 3: 10.7 usec per loop

... мы можем получить почти обратно к легко полученной производительности простейшего и наиболее очевидного, компактного и читаемого подхода (простой, элементарный, тривиальный цикл). Это указывает на еще одну проблему с lambda, на самом деле: производительность! Для достаточно простых операций, таких как умножение, накладные расходы на вызов функции довольно значительны по сравнению с фактической выполняемой операцией - reducemap и filter) часто заставляет вас вставлять такой вызов функции, где простые циклы, представления списков и выражения генератора, позволяют читаемость, компактность и скорость оперативных операций.

Возможно, даже хуже, чем вышеописанное "назначить лямбда для имени", анти-идиома на самом деле является следующим анти-идиомом, например. для сортировки списка строк по их длине:

thelist.sort(key=lambda s: len(s))

вместо очевидного, читаемого, компактного, более быстрого

thelist.sort(key=len)

Здесь использование lambda ничего не делает, кроме вставки уровня косвенности - без какого-либо хорошего эффекта и большого числа плохих.

Мотивация использования lambda часто позволяет использовать map и filter вместо более предпочтительного понимания цикла или списка, которое позволит вам делать простые, нормальные вычисления в строке; вы все равно платите этот "уровень косвенности", конечно. Это не Pythonic, чтобы задаться вопросом: "Должен ли я использовать список или карту здесь": просто всегда используйте listcomps, когда они кажутся применимыми, и вы не знаете, какой из них выбрать, на основе "должен быть один и предпочтительно только один, очевидный способ сделать что-то". Вы часто пишете listcomps, которые не могут быть разумно переведены на карту (вложенные циклы, предложения if и т.д.), В то время как нет вызова map, который не может быть правильно переписан как listcomp.

Совершенно правильные функциональные подходы в Python часто включают в себя понимание списков, генераторные выражения, itertools, функции более высокого порядка, функции первого порядка в разных обличьях, замыканиях, генераторах (а иногда и другие итераторы).

itertools, как отметил комментатор, включает imap и ifilter: разница в том, что, как и все itertools, они основаны на потоках (например, map и filter встроенные в Python 3, но отличается от тех, что были встроены в Python 2). itertools предлагает набор строительных блоков, которые хорошо сочетаются друг с другом и великолепная производительность: особенно если вы обнаружите, что имеете возможность работать с очень длинными (или даже неограниченными!) последовательностями, вы обязаны сделать это для себя, чтобы ознакомиться с itertools - - их целая глава в документах делает для хорошего чтения, а recipes, в частности, весьма поучительны.

Написание собственных функций более высокого порядка часто полезно, особенно когда они подходят для использования в качестве декораторов (оба декоратора функций, как объяснено в этой части документов и декораторов классов, введенных в Python 2.6). Не забывайте использовать functools.wraps на ваших декораторах функций (чтобы сохранить метаданные обернутой функции)!

Итак, суммируя...: все, что вы можете кодировать с помощью lambda, map и filter, вы можете кодировать (чаще, чем не выгодно) с помощью def (named functions) и listcomps - и, как правило, перемещение на одну ступень до генераторов, генераторных выражений или itertools, еще лучше. reduce соответствует юридическому определению "привлекательной неприятности"...: вряд ли когда-либо правильный инструмент для работы (почему он не является встроенным в Python 3, наконец-то! -).

Ответ 2

FP важна не только для concurrency; фактически, в канонической реализации Python практически нет concurrency (возможно, изменения 3.x, которые?). в любом случае, FP хорошо подходит для concurrency, потому что это приводит к программам без или меньших (явных) состояний. штаты хлопотны по нескольким причинам. один заключается в том, что они делают распределение вычислений жестким (er) (что аргумент concurrency), другой, гораздо более важный в большинстве случаев, является тенденцией к наложению ошибок. самым большим источником ошибок в современном программном обеспечении являются переменные (существует тесная взаимосвязь между переменными и состояниями). FP может уменьшить количество переменных в программе: ошибки раздавлены!

посмотрите, сколько ошибок вы можете внести, смешав переменные в этих версиях:

def imperative(seq):
    p = 1
    for x in seq:
        p *= x
    return p

versus (предупреждение, my.reduce список параметров отличается от списка параметров python reduce; объяснение ниже)

import operator as ops

def functional(seq):
    return my.reduce(ops.mul, 1, seq)

как вы можете видеть, это факт, что FP дает вам меньше возможностей стрелять в ногу с ошибкой, связанной с переменными.

также читаемость: может потребоваться немного обучения, но functional легче читать, чем imperative: вы видите reduce ( "ok, это уменьшает последовательность до одного значения" ), mul ( "умножением" ). wherease imperative имеет общую форму цикла for, переполненную переменными и назначениями. эти циклы for выглядят одинаково, поэтому, чтобы понять, что происходит в imperative, вам нужно прочитать почти все.

то есть суткость и гибкость. вы даете мне imperative, и я говорю вам, что мне это нравится, но хочу что-то суммировать последовательности. вы не говорите, и вы уходите, скопируйте:

def imperative(seq):
    p = 1
    for x in seq:
        p *= x
    return p

def imperative2(seq):
    p = 0
    for x in seq:
        p += x
    return p

что вы можете сделать, чтобы уменьшить дублирование? ну, если операторы были значениями, вы могли бы сделать что-то вроде

def reduce(op, seq, init):
    rv = init
    for x in seq:
        rv = op(rv, x)
    return rv

def imperative(seq):
    return reduce(*, 1, seq)

def imperative2(seq):
    return reduce(+, 0, seq)

О, подождите! operators предоставляет операторы, которые являются значениями! но.. Алекс Мартелли осудил reduce уже... похоже, если вы хотите остаться в пределах границ, которые он предлагает, вы обречены на копирование вставки сантехники.

версия FP лучше? вам тоже нужно будет скопировать-вставить?

import operator as ops

def functional(seq):
    return my.reduce(ops.mul, 1, seq)

def functional2(seq):
    return my.reduce(ops.add, 0, seq)

ну, это всего лишь артефакт полуподобного подхода! отказываясь от императива def, вы можете сжимать обе версии до

import functools as func, operator as ops

functional  = func.partial(my.reduce, ops.mul, 1)
functional2 = func.partial(my.reduce, ops.add, 0)

или даже

import functools as func, operator as ops

reducer = func.partial(func.partial, my.reduce)
functional  = reducer(ops.mul, 1)
functional2 = reducer(ops.add, 0)

(func.partial является причиной my.reduce)

как насчет скорости выполнения? да, использование FP на языке, таком как Python, будет иметь некоторые накладные расходы. здесь я буду просто попугать, что несколько профессоров должны сказать об этом:

  • преждевременная оптимизация - это корень всего зла.
  • большинство программ тратят 80% своей рабочей среды на 20% своего кода.
  • не спекулируйте!

Я не очень хорошо объясняю вещи. Не позволяйте мне слишком сильно мутить воду, читайте первую половину речи Джон Бэкус дал по случаю получения премии Тьюринга в 1977 году Цитата:

5.1 Программа фон Неймана для внутреннего продукта

c := 0
for i := I step 1 until n do
   c := c + a[i] * b[i]

Несколько свойств этой программы: стоит отметить:

  • Его утверждения действуют на невидимом "состоянии" по сложному правила.
  • Это не иерархическая. За исключением правой части задания, он не создает сложные объекты из более простых. (Однако чаще выполняются большие программы.)
  • Он динамичен и повторяется. Нужно мысленно выполнить его понять это.
  • Он вычисляет слово по времени путем повторения (задания) и модификация (переменной i).
  • Часть данных, n, находится в программе; поэтому ему не хватает общности и работает только для векторов длины n.
  • Он называет свои аргументы; его можно использовать только для векторов a и b. Чтобы стать общим, требуется декларация процедуры. Они включают сложные проблемы (например, вызов по имени по сравнению с call-by-value).
  • Операции "домашнего хозяйства" представлены символами в разбросанные места (в заявлении for и индексы в присваивании). Это делает невозможным консолидировать хозяйственные операции, наиболее распространенный из всех, мощных, широко используемых операторов. Таким образом, при программировании этих операций всегда нужно начинать снова с квадрата один, написание "for i := ..." и "for j := ...", за которым следует присваивания, посыпанные i и j.

Ответ 3

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

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

Ответ 4

Этот ответ полностью переработан. Он содержит множество наблюдений из других ответов.

Как вы можете видеть, существует множество сильных чувств, связанных с использованием конструктов функционального программирования в Python. Здесь есть три основные группы идей.

Во-первых, почти все, кроме людей, которые наиболее привязаны к чистому выражению функциональной парадигмы, согласны с тем, что понимание списков и генераторов лучше и яснее, чем использование map или filter. Вашим коллегам следует избегать использования map и filter, если вы нацеливаете версию Python на новую, чтобы поддерживать списки. И вам следует избегать itertools.imap и itertools.ifilter, если ваша версия Python достаточно новая для понимания генератора.

Во-вторых, в сообществе в целом существует много амбивалентности около lambda. Многие люди действительно раздражены синтаксисом в дополнение к def для объявления функций, особенно тех, которые включают ключевое слово, подобное lambda, которое имеет довольно странное имя. И люди также недовольны тем, что в этих небольших анонимных функциях отсутствует какая-либо из приятных метаданных, описывающих любую другую функцию. Это делает отладку сложнее. Наконец, небольшие функции, объявленные lambda, часто не очень эффективны, поскольку они требуют накладных расходов на вызов функции Python при каждом вызове, который часто находится во внутреннем цикле.

Наконец, большинство (что означает > 50%, но, скорее всего, не 90%), люди думают, что reduce немного странно и неясно. Я сам признаю наличие print reduce.__doc__ всякий раз, когда я хочу его использовать, что не так часто. Хотя, когда я вижу, что он используется, природа аргументов (например, функция, список или итератор, скаляр) говорят сами за себя.

Что касается меня, я попадаю в лагерь людей, которые считают, что функциональный стиль часто очень полезен. Но балансирование этой мысли заключается в том, что Python не является сердцем функциональным языком. И чрезмерное использование функциональных конструкций может заставить программы казаться странно искаженными и трудными для понимания людьми.

Чтобы понять, когда и где функциональный стиль очень полезен и улучшает читаемость, рассмотрите эту функцию в С++:

unsigned int factorial(unsigned int x)
{
    int fact = 1;
    for (int i = 2; i <= n; ++i) {
        fact *= i;
    }
    return fact
 }

Этот цикл кажется очень простым и понятным. И в этом случае. Но его кажущаяся простота - ловушка для неосторожного. Рассмотрим это альтернативное средство записи цикла:

unsigned int factorial(unsigned int n)
{
    int fact = 1;
    for (int i = 2; i <= n; i += 2) {
        fact *= i--;
    }
     return fact;
 }

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

Это давно признанная проблема, и в написании питона такой цикл довольно неестественен. Вы должны использовать цикл while, и это выглядит неправильно. Вместо этого в Python вы должны написать что-то вроде этого:

def factorial(n):
    fact = 1
    for i in xrange(2, n):
        fact = fact * i;
    return fact

Как вы можете видеть, способ, которым вы говорите о переменной управления контуром в Python, не подлежит обманыванию внутри цикла. Это устраняет множество проблем с "умными" петлями на других императивных языках. К сожалению, это идея, которая заимствована из функциональных языков.

Даже это поддается странной игре. Например, этот цикл:

c = 1
for i in xrange(0, min(len(a), len(b))):
    c = c * (a[i] + b[i])
    if i < len(a):
        a[i + 1] = a[a + 1] + 1

Упс, мы снова имеем петлю, которую трудно понять. Это поверхностно напоминает действительно простой и понятный цикл, и вы должны внимательно его прочитать, чтобы понять, что одна из переменных, используемых в вычислении цикла, запуталась таким образом, что это повлияет на будущие прогоны цикла.

Опять же, более функциональный подход к спасению:

from itertools import izip
c = 1
for ai, bi in izip(a, b):
   c = c * (ai + bi)

Теперь, посмотрев на код, у нас есть сильная индикация (частично из-за того, что человек использует этот функциональный стиль), что списки a и b не изменяются во время выполнения цикла. О чем меньше думать.

Последнее, о чем нужно беспокоиться, - это странное изменение. Возможно, это глобальная переменная и меняет какой-то вызов функции круговой обхода. Чтобы избавить нас от этого психического беспокойства, здесь есть чисто функциональный подход:

from itertools import izip
c = reduce(lambda x, ab: x * (ab[0] + ab[1]), izip(a, b), 1)

Очень кратким, и структура говорит нам, что x является чисто аккумулятором. Это локальная переменная во всем мире. Конечный результат однозначно присваивается c. Теперь о чем беспокоиться гораздо меньше. Структура кода удаляет несколько классов возможных ошибок.

Вот почему люди могут выбрать функциональный стиль. Это краткий и понятный, по крайней мере, если вы понимаете, что делают reduce и lambda. Существуют большие классы проблем, которые могут повлиять на программу, написанную в более императивном стиле, который, как вы знаете, не повлияет на вашу программу функционального стиля.

В случае factorial существует очень простой и понятный способ записи этой функции в Python в функциональном стиле:

import operator
def factorial(n):
    return reduce(operator.mul, xrange(2, n+1), 1)

Ответ 5

Вопрос, который, по-видимому, в основном игнорируется здесь:

действительно ли программирование Python действительно помогает с помощью concurrency?

Нет. Значение FP, приходящее на concurrency, заключается в устранении состояния при вычислении, что в конечном итоге отвечает за труднодоступную гадость непреднамеренных ошибок при параллельном вычислении. Но это зависит от параллельных идиом программирования, которые сами по себе не являются состояниями, что не относится к Twisted. Если есть concurrency идиомы для Python, которые используют программное обеспечение без состояния, я не знаю о них.

Ответ 6

Вот краткий обзор положительных ответов, когда/почему программировать функционально.

  • Список понятий был импортирован из Haskell, языка FP. Они Pythonic. Я бы предпочел написать
y = [i*2 for i in k if i % 3 == 0]

чем использовать императивную конструкцию (цикл).

  • Я бы использовал lambda, когда задал сложный ключ sort, например list.sort(key=lambda x: x.value.estimate())

  • Чистое использование функций более высокого порядка, чем для написания кода с использованием шаблонов проектирования ООП, таких как visitor или abstract factory

  • Люди говорят, что вы должны программировать Python в Python, С++ в С++ и т.д. Это правда, но, безусловно, вы должны думать по-разному в одном и том же. Если во время написания цикла вы знаете, что вы действительно уменьшаете (сворачиваете), тогда вы сможете думать на более высоком уровне. Это очищает ваш разум и помогает организовать. Конечно, мышление нижнего уровня тоже важно.

Вы не должны чрезмерно использовать эти функции - есть много ловушек, см. сообщение Alex Martelli. Я бы субъективно сказал, что самая серьезная опасность заключается в том, что чрезмерное использование этих функций приведет к разрушению читаемости вашего кода, который является основным атрибутом Python.

Ответ 7

Стандартные функции filter(), map() и reduce() используются для различных операций над списком, и все три функции ожидают два аргумента: функция и список

Мы могли бы определить отдельную функцию и использовать ее в качестве аргумента для filter() и т.д., и, вероятно, это хорошая идея, если эта функция используется несколько раз или если функция слишком сложна для записи в одной строке, Однако, если это необходимо только один раз, и это довольно просто, удобнее использовать lambda-конструкцию для создания (временной) анонимной функции и передать ее в filter().

Это помогает в readability and compact code.

Используя эту функцию, также окажется efficient, потому что цикл в элементах списка выполняется в C, что немного быстрее, чем цикл в python.

И объектно-ориентированный способ принудительно необходим, когда состояния должны поддерживаться, кроме абстракции, группировки и т.д. Если требование довольно простое, я буду придерживаться функциональности, а не объектно-ориентированного программирования.

Ответ 8

Карта и фильтр занимают место в программировании OO. Прямо рядом со списком понятий и функций генератора.

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

Лямбда никогда. Лямбда бесполезна. Можно сделать аргумент, что он на самом деле что-то делает, поэтому он не совсем бесполезен. Во-первых: Лямбда не является синтаксическим "сахаром"; это делает вещи больше и уродливее. Во-вторых: однократно в 10 000 строк кода, которые считают, что вам нужна "анонимная" функция, превращается в два раза в 20 000 строк кода, что устраняет ценность анонимности, что делает ее ответственностью за обслуживание.

Однако.

Функциональный стиль программирования без изменения состояния объекта по-прежнему носит OO. Вы просто делаете больше создания объектов и меньше обновлений объектов. После того, как вы начнете использовать функции генератора, многое программирует OO в функциональном направлении.

Кажется, что каждое изменение состояния преобразуется в функцию генератора, которая строит новый объект в новом состоянии из старого объекта (ов). Это интересное мировоззрение, потому что рассуждение об алгоритме намного, намного проще.

Но для этого нет необходимости использовать сокращение или лямбда.