Читая практическое введение Мэри Роуз Кук в функциональное программирование , она приводит пример анти-паттерна.
def format_bands(bands):
for band in bands:
band['country'] = 'Canada'
band['name'] = band['name'].replace('.', '')
band['name'] = band['name'].title()
поскольку
- функция делает больше чем одно
- имя не является описательным
- у него есть побочные эффекты
В качестве предложенного решения она предлагает конвейеризацию анонимных функций
pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
call(lambda x: x.replace('.', ''), 'name'),
call(str.title, 'name')])
Однако, мне кажется, у этого есть и обратная сторона: он еще менее проверяем; по крайней мере, у format_bands может быть модульный тест, чтобы проверить, выполняет ли он то, для чего предназначен, но как протестировать конвейер? Или идея о том, что анонимные функции настолько очевидны, что их не нужно проверять?
Мое реальное приложение для этого - попытаться сделать мой pandas
код более функциональным. У меня часто будет какой-то конвейер внутри функции
def munge_data(df)
df['name'] = df['name'].str.lower()
df = df.drop_duplicates()
return df
Или переписать в стиле конвейера:
def munge_data(df)
munged = (df.assign(lambda x: x['name'].str.lower()
.drop_duplicates())
return munged
Любые предложения для лучших практик в такой ситуации?
python
unit-testing
Макс Фландер
источник
источник
Ответы:
Я думаю, что вы пропустили, вероятно, более важную часть исправленного примера книги. Более фундаментальное изменение кода - от метода, работающего со всеми значениями в списке, до работы с одним элементом.
Уже существуют такие функции, как
iter
(в данном случае именованныеpipeline_foreach
), которые выполняют данную операцию над всеми элементами в списке. Там не было необходимости дублировать это сfor
петлей. Также использование хорошо известной операции со списком проясняет ваши намерения. Сmap
вами преображаются ценности. При этомiter
вы выполняете побочный эффект с каждым элементом. Сfor
циклом вы ... ну, вы действительно не знаете, пока не просмотрите его.Исправленный в примере код все еще не очень функционален, потому что он (насколько я могу судить) изменяет значения в списке, не возвращая их, предотвращая дальнейшее создание конвейера или состав функции. Функционально предпочтительный метод
map
создаст новый список полос с обновленнымиcountry
иname
. Затем вы можете передать этот вывод следующей функции илиmap
создать другую функцию, которая заняла список диапазонов. Сiter
, это как трубопроводный тупик.Я думаю, что код конечного результата имеет небольшие функции, которые слишком тривиальны, чтобы беспокоиться о тестировании здесь. В конце концов, вам не нужно писать модульные тесты против
replace
илиtitle
. Теперь, возможно, вы захотите объединить их в свои собственные функции и модульный тест, чтобы получить желаемую комбинацию для одного элемента. Я, наверное, просто перешел быformat_bands
вformat_band
единственное число, отбросил цикл for и позвонилpipeline_each(bands, format_band)
. Затем вы можете проверить format_band, чтобы убедиться, что вы ничего не забыли.Во всяком случае, на ваш код. Ваш второй пример кода кажется более конвейерным. Но это само по себе не дает преимуществ функционального программирования. На практике функциональное программирование означает обеспечение совместимости функций с другими функциями путем определения их совместимости только с точки зрения их входов и выходов. Если внутри функции есть скрытые побочные эффекты, то, несмотря на то, что ее вход / выход совпадают с другой функцией, вы не можете знать, совместимы ли они до времени выполнения. Однако, если две функции не имеют побочных эффектов и соответствуют выводу-вводу, вы можете конвейеризовать или скомпоновать их, не беспокоясь о неожиданных результатах.
источник