На работе мы использовали программу Python в довольно стандартном способе OO. В последнее время пара парней попала в функциональную подножку. И их код теперь содержит много больше лямбда, карты и уменьшает. Я понимаю, что функциональные языки хороши для concurrency, но действительно ли программирование Python действительно помогает с помощью concurrency? Я просто пытаюсь понять, что получаю, если я начну использовать больше функциональных возможностей Python.
Зачем программировать функционально в Python?
Ответ 1
Изменить. Я был привлечен к задаче в комментариях (частично, по-видимому, фанатиками FP на Python, но не исключительно), чтобы не давать больше объяснений/примеров, поэтому, расширение ответа на поставку некоторых.
lambda
, тем более map
(и filter
), и, в особенности 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
, на самом деле: производительность! Для достаточно простых операций, таких как умножение, накладные расходы на вызов функции довольно значительны по сравнению с фактической выполняемой операцией - reduce
(и map
и 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 в функциональном направлении.
Кажется, что каждое изменение состояния преобразуется в функцию генератора, которая строит новый объект в новом состоянии из старого объекта (ов). Это интересное мировоззрение, потому что рассуждение об алгоритме намного, намного проще.
Но для этого нет необходимости использовать сокращение или лямбда.