functools (это такая свалка для всяких ненужных мне вещей :-).
— Гвидо ван Россум
Может показаться, что статья о ФП, но я не собираюсь обсуждать парадигму. Речь пойдет о переиспользовании и упрощении кода — я попытаюсь доказать, что вы пишете слишком много кода, поэтому он сложный и тяжело тестируется, но самое главное: его долго читать и менять.
В статье заимствуются примеры и/или концепции из библиотеки funcy. Во-первых, она клевая, во-вторых, вы сразу же сможете начать ее использовать. И да, нам понадобится ФП.
Кратко о ФП
- чистые функции
- функции высшего порядка
- чувство собственного превосходства над теми, кто пишет не функционально (необязательно)
ФП также присущи следующие приемы:
- частичное применение
- композирование (в python еще есть декораторы)
- ленивые вычисления
Если вам все это уже знакомо, переходите сразу к примерам.
Чистые функции
Чистые функции зависят только от своих параметров и возвращают только свой результат. Следующая функция вызванная несколько раз с одним и тем же аргументом выдаст разный результат (хоть и один и тот же объект, в данном случае %).
Напишем функцию-фильтр, которая возвращает список элементов с тру-значениями.
pred = bool
result = []
def filter_bool(seq):
for x in seq:
if pred(x):
result.append(x)
return result
Сделаем ее чистой:
pred = bool
def filter_bool(seq):
result = []
for x in seq:
if pred(x):
result.append(x)
return result
Теперь можно вызвать ее лярд раз подряд и результат будет тот же.
Функции высшего порядка
Это такие функции, которые принимают в качестве аргументов другие функции или возвращают другую функцию в качестве результата.
def my_filter(pred, seq):
result = []
for x in seq:
if pred(x):
result.append(x)
return result
Мне пришлось переименовать функцию, потому что она теперь куда полезнее:
above_zero = my_filter(bool, seq)
only_odd = my_filter(is_odd, seq)
only_even = my_filter(is_even, seq)
Заметьте, одна функция и делает уже много чего. Вообще-то, она должна быть ленивой, делаем:
def my_filter(pred, seq):
for x in seq:
if pred(x):
yield x
Вы заметили, что мы удалили код, а стало только лучше? Это лишь начало, скоро мы будем писать функции только по праздникам. Вот смотрите:
my_filter = filter
Встроенных возможностей python почти хватает для полноценной жизни, нужно лишь их грамотно компоновать.
Частичное применение
Это процесс фиксации части аргументов функции, который создает другую функцию, меньшей арности. В переводе на наш это functools.partial
.
filter_bool = partial(filter, bool)
filter_odd = partial(filter, is_odd)
filter_even = partial(filter, is_even)
Я понимаю, что это все азы ФП, но хочу отметить, что мы не написали ничего нового: мы взяли уже готовые функции и сделали другие. Основа новых — очень маленькие, простые, легкотестируемые функции, мы можем без опаски использовать их для создания более сложных.
Композирование
Такой простой, крутой и нужной штуки в python нет. Ее можно написать самостоятельно, но хотелось бы вменяемой сишной имплементации :(
def compose(*fns):
init, *rest = reversed(fns)
return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))
Теперь мы можем делать всякие штуки (выполнение идет справа налево):
mapv = compose(list, map)
filterv = compose(list, filter)
Это прежние версии map
и filter
из второй версии python. Теперь, если вам понадобится неленивый map
, вы можете вызвать mapv
. Или по старинке писать чуть больше кода. Каждый раз.
Функции compose
и partial
прекрасны тем, что позволяют переиспользовать уже готовые, оттестированные функции. Но самое главное, если вы понимаете преимущество данного подхода, то со временем станете сразу писать их готовыми к композиции.
Это очень важный момент — функция должна решать одну простую задачу, тогда:
- она будет маленькой
- ее будет проще тестировать
- легко композировать
- просто читать и менять
- тяжело сломать
Пример
Задача: дропнуть None
из последовательности.
Решение по старинке (чаще всего даже не пишется в виде функции):
no_none = (x for x in seq if x is not None)
Обратите внимание: без разницы как называется переменная в выражении. Это настолько неважно, что большинство программистов тупо пишут x
, чтобы не заморачиваться. Все пишут этот бессмысленный код раз за разом. Каждый цензура раз: for
, in
, if
и несколько раз x
— потому что для компрехеншена нужен scope и у него есть свой синтаксис. Мы пишем: на каждую итерацию цикла присвоить переменной значение. И оно присваивается, и проверяется условие.
Мы каждый раз пишем этот бойлерплейт и пишем тесты на этот бойлерплейт. Зачем?
Давайте перепишем:
from operator import is_
from itertools import filterfalse
from functools import partial
is_none = partial(is_, None)
filter_none = partial(filterfalse, is_none)
# Использование
no_none = filter_none(seq)
# Переиспользование
all_none = compose(all, partial(map, is_none))
Все. Никакого лишнего кода. Мне приятно такое читать, потому что этот код (no_none = filter_none(seq)
) очень простой. То, как работает это функция, мне нужно прочитать ровно один раз за все время в проекте. Компрехеншен вам придется читать каждый раз, чтобы точно понять что оно делает. Ну или засуньте ее в функцию, без разницы, но не забудьте про тесты.
Пример 2
Довольно частая задача получить значения по ключу из массива словарей.
names = (x['name'] for x in users)
Кстати, работает очень быстро, но мы снова написали кучу ненужной фигни. Перепишем, чтобы работало еще быстрее:
from operator import itemgetter
def pluck(key, seq):
return map(itemgetter(key), seq)
# Использование
names = pluck('name', users)
А как часто мы это будем делать?
get_names = partial(pluck, 'name')
get_ages = partial(pluck, 'age')
# Сложнее
get_names_ages = partial(pluck, ('name', 'age'))
users_by_age = compose(dict, get_names_ages)
ages = users_by_ages(users) # {x['name']: x['age'] for x in users}
А если у нас объекты? Пф, параметризируй это:
from operator import itemgetter, attrgetter
def plucker(getter, key, seq):
return map(getter(key), seq)
pluck = partial(plucker, itemgetter)
apluck = partial(plucker, attrgetter)
# Использование
names = pluck('name', users) # (x['name'] for x in users)
object_names = apluck('name', users) # (x.name for x in users)
# Геттеры умеют сразу таплы данных
object_data = apluck(('name', 'age', 'gender'), users) # ((x.name, x.age, x.gender) for x in users)
Пример 3
Представим себе простой генератор:
def dumb_gen(seq):
result = []
for x in seq:
# здесь что-то проиcходит
result.append(x)
return result
Тут полно бойлерплейта: мы создаем пустой список, затем пишем цикл, добавляем элемент в список, отдаем его. Кажется, я буквально перечислил все тело функции :(
Правильным решением будут использование filter(pred, seq)
или map(func, seq)
, но иногда нужно сделать что-то сложнее, т.е. генератор написать действительно нужно. А если результат всегда нужен в виде списка или тапла? Да легко:
@post_processing(list)
def dumb_gen(seq):
for x in seq:
...
yield x
Это параметрический декоратор, работает он так:
result = post_processing(list)(dumb_gen)(seq)
Т.е. результатом первого вызова будет новая функция, которая примет функцию в качестве аргумента и вернет другую функцию. Звучит сложнее, чем есть:
def post_processing(post):
return lambda func: compose(post, func)
Обратите внимание, я использовал уже существующую compose
. Результат — новая функция, которую никто не писал.
А теперь стихи:
post_list = post_processing(list)
post_tuple = post_processing(tuple)
post_set = post_processing(set)
post_dict = post_processing(dict)
join_comma = post_processing(', '.join)
@post_list
def dumb_gen(pred, seq):
for x in seq:
...
yield x
Куча новых функций по цене одной! И я убрал бойлерплейт, функция стала меньше и намного симпатичнее.
Итог
Перебирая данные железобетонными функциями (чистыми, высшими), мы сохраняем простоту реализации и обеспечиваем стабильность программы, которую проще тестировать:
- пишите чистые функции, они обеспечат стабильность программы
- пишите функции высшего порядка, код станет намного компактнее и надежнее
- композируйте, декорируйте, частично применяйте, переиспользуйте код
- используйте сишные либы, они дадут скорости вашему софту
Как только вы напишете свой набор инструментов, новый код будет создаваться со знанием того, что у вас есть штука, которая может решить часть задачи. А значит софт будет меньше и проще.
С чего начать?
- обязательно ознакомьтесь с itertools, functools, operator, collections, в особенности с примерами в конце
- загляните в документацию funcy или другой фпшной либы, почитайте исходный код
- напишите свой
funcy
, весь он сразу вам не нужен, но опыт очень пригодится
Credits
В моем случае, использование ФП началось со знакомства с clojure — это штука капитально выворачивает мозги, настоятельно рекомендую посмотреть хотя бы видосы на ютубе.
Clojure как-то так устроен, что вам приходится писать проще, без привычных нам вещей: без переменных, без любимого стиля "романа", где сначала мы раскрываем личность героя, потом пускаемся в его сердечные проблемы. В clojure вам приходится думать %) В нем только базовые типы данных и "отсутствие синтаксиса" (с). И эту "простую" концепцию, оказывается, можно портировать в python.
UPD
Похоже, у читателей сложилось впечатление, будто я пишу сплошным ФП. Хочу всех успокоить: функциональный подход я использую исключительно в местах, где пишется код, который я уже писал. На мой взгляд, повторять "рабочие" приемы всякий раз глупо и бессмысленно, поэтому перевожу подобные куски в функции и использую их повторно. Рабочий пример можно посмотреть в комментарии.
Комментарии (88)
mistiman
09.01.2018 22:39Боюсь за такие "упрощения" коллеги меня будут бить. Код становится не читаемым без видимых на то причин. Проще лучше
magic4x Автор
09.01.2018 22:46Дело привычки, поверьте.
mistiman
10.01.2018 00:20Боюсь я был не понят) Люблю фп, но там где он реально уместен. «Упрощать» сomprehensions через создание функции это что-то лишнее.
сomprehensions прочитает любой знакомый с питоном, а чтобы развернуть в голове вот эти функции нужно либо знать все использованные функции высшего порядка, либо все их просмотреть, а значит неоднократно переместиться по коду проекта.
Лучше пусть будет на 2,3,5 строк больше, но чтобы это читалось проще.
pfemidi
09.01.2018 23:13Для функциональщиков это может быть и проще, но для императивщиков… Я лично
глазамозги сломал пока разложил всё это мысленно в обычные императивные функции, понял как это работает и понял что лично я так никогда делать не буду :-)lxsmkv
10.01.2018 00:50Согласен. Сам стараюсь писать так, чтобы те кто с языком не знакомы могли понимать как код работает. В ущерб самолюбию. Это моя интерпретация догмы «Explicit is better than implicit» — сторониться неочевидного.
—
«Если долго вглядываться в код, увидишь, что это всего лишь набор символов.» © Я.
0x18h
09.01.2018 23:07Теперь можно вызвать ее лярд раз под ряд и результат будет тот же.
А разве это чистая функция раз её результат зависит от глобальной переменной pred?
lgorSL
10.01.2018 00:31Мне кажется, проблема синтаксиса Питона в том, что код типа такого:
no_none = (x for x in seq if x is not None)
пишется короче, чем в функциональном стиле
no_none = filter(lambda x: x is not None, seq)
Всё равно придётся написать 'x' целых два раза и ещё слово lambda появится. Если захотеть, чтобы no_none была списком, а не генератором, то станет ещё хуже:
no_none = [x for x in seq if x is not None]
vs
no_none = list(filter(lambda x: x is not None, seq))
Появилось ещё одно слово и вложенные скобочки. Нельзя просто так взять и написать на питоне красиво и функционально — чтобы получить какой-то выигрыш в краткости, надо брать что-то реально повторяющееся.
В некоторых языках происходит наоборот — они подталкивают к функциональному стилю как более простому и короткому, например:
val no_none = seq filter (_ != null)
Я время от времени порываюсь написать что-то функциональное на питоне, но почти всё время остаётся чувство, что проще и короче написать решение "в лоб".
DaneSoul
10.01.2018 12:04Почему Вы противопоставляете comprehensions и функциональный стиль?
Оно даже в официальном мануале идет в разделе функционального прогаммирования: docs.python.org/3/howto/functional.html
List comprehensions and generator expressions are a concise notation for such operations, borrowed from the functional programming language Haskell
potan
10.01.2018 21:52В питоне comprehensions реализован не совсем функционально — он образается с переменной как с мутабельной:
Python 3.3.5 (default, Dec 11 2015, 11:33:43) [MSC v.1800 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> x = [lambda x: x+i for i in [1,2,3,4]] >>> x[0](1) 5
А во python2 даже портит ее в скопе.
evgenyk
10.01.2018 01:04Для меня идеальный код, это когда его читаешь как текст.
И кстати, зачем в первом примере pred = bool глобальная переменная?trapwalker
10.01.2018 14:34Полагаю автор хотел довести нас к прозрению малыми шагами, а вышло как всегда: кто и без того понимал суть — стали придираться; кому оно не сильно надо — всё равно споткнулись и оставили; наверно, единицы кому зашло с благодарностью улыбнулись и пошли дальше.
Автор молодец, он придумал хорошие примеры, обосновал их полезность, разжевал и отполировал изложение. Кому-то будет полезно наверняка. Я лично почерпнул лишь некоторые тонкости манеры изложения, и оно того стоило, знаете ли. Какой-нибудь пример обязательно пригодится, когда в очередной раз придётся рассказывать молодым коллегам основы.
Lure_of_Chaos
10.01.2018 01:06Извините, но не нужно из python делать clojure (извините, lisp). Да, чистые функции предсказуемы, тестируемы, хорошо параллелятся. Да, функции высшего порядка более универсальны. Да, map reduce легче читается, чем for if for for.
Но все же надо знать меру, и не нужно вводить кучу новых функций ради композиции и каррирования, не нужно более читаемые for if for for заменять совершенно нечитаемыми вложенными скобками, не нужно в худших традициях ФП вместо трансформации списков заниматься их копированием.
Возьмите лучшее из обоих миров и спокойно пишите понятный код, не наживая себе хаскель головного мозга, не беспокоясь о том, что где-то не слишком Функционально.
Ну а за попытку из изначально императивного (знаю, мультипарадигменного, но все же) языка сделать чисто функциональный — статье плюс.trapwalker
10.01.2018 14:47Ой, не сгущайте краски, коллега. Вы, конечно правы, но никто никого, как мне кажется, не пытается заставить превращать один язык в другой. Понимать концепцию всегда полезно и для новичков, например, через питон это сделать. может быть будет иногда лучше.
Я тоже с трудом вижу где бы такой перефункционализированный подход улучшил питоновский код в обыденной жизни. Однако, как говорится, хорошо подобранным примером можно доказать всё что угодно.
Давайте напряжемся и придумаем за автора (как адвокаты дьявола) пример в его пользу. Это же интересно.
Мне приходит на ум что-то вроде задач сложной настраиваемой обработки потоков данных, когда набор преобразований, применяемых к потоку, требуется сделать кастомизируемым, прозрачным и поддающимся контролю. Я про те самые случаи. когда ООП с его состояниями побочными эффектами плавно превращается в геморрой из фабрик, куч, пуллов, очередей и прочего. Не знаю даже. Подумаю еще=).magic4x Автор
10.01.2018 17:15Отвечу сразу на два ваших комментария: во-первых, спасибо за лестный отзыв выше, очень непросто написать статью и учесть знания/опыт всей аудитории, и уместить это в размере поста; во-вторых, реальный пример:
@post_mapping(foo) def bar(self, data): yield from cat(keep('key', data.values())) yield self.baz
Тут, конечно, потребуется знания этих странных функций. Но в любой команде рано или поздно появляется свой набор утилит, которыми пользуются все. В нашем случае это набор функциональных тулов аналогичных
funcy
. Аналогичных, потому что у нас они работают чуточку иначе.
И так:
post_mapping
вызоветfoo
на каждый элемент отданным генератором, аналогmap(foo, bar())
. Только не придется писать это всякий разcat
— это шорткат кitertools.chain.from_iterable
— склеивает массивы вместе в одинkeep
— это комбинация "достань по ключуkey
и дропни фолс-значения"
По порядку:
- из значений словаря по ключу
key
достаются значения (в данном случае это массивы), затем удаляются пустые, затем объединяются в один и отдаются - в хвост генератора добавляется
self.baz
- все элементы обрабатываются функцией
foo
Императивно это выглядит раза в 4 больше. И дело тут не в размере, а в том, что используя одни и те же инструменты (и понимая как они работают), вам не нужно читать и писать код, который делает то же самое, но конкретно тут и конкретно так каждый раз.
И оно ленивое!
aavezel
10.01.2018 09:10+10На улице 2018… А всё как 8 лет назад — по основам функционального кода в питоне пишут статьи, а в комментах идет срач, о том что императивный стиль понятнее…
domix32
10.01.2018 10:46-1Не понял зачем писать собственные варианты фильтра, когда в области видимости без импортов уже есть `map`, `filter` и `reduce`.
brake
10.01.2018 11:59-1Вероятно для того, чтобы создать самодокументированный код (к чему очень стимулирует, хотя бы, clojure). Типа вместо
filter(много параметров)
, который затрудняет чтение кода, мы, сначала определяемfilter_none()
и потом его используем как предикат. Читающему глазу уже становится легче.magic4x Автор
10.01.2018 13:22-1Все проще: map, filter и reduce питонисты не используют. Я серьезно.
KMiNT21
10.01.2018 13:40+2Ну зачем же за всех говорить. :) Вот лично я filter всегда использую, а map — из модуля multiprocessing в виде multiprocessing.Pool().map
Плюс записи вроде «sume_func = lambda x: .....xyz» внутри функций, чтобы ничего лишнего оттуда не выносить «наверх».
domix32
10.01.2018 14:13Причины не использовать их?
myrslok
10.01.2018 15:42Comprehensions идиоматичнее.
evocatus
11.01.2018 11:12Спорно. К тому же если использовать модуль operator, то map, filter и reduce становятся короче и легче читаются, чем лямбды (у них в Python настолько уродский синтаксис — если хотите поспорить, то посмотрите на синтаксис лямбд в том же Clojure, не говоря уже про haskell, где даже обычные функции объявляются лаконичнее, чем лямбды в Python).
P.S. Напоминаю, что list comprehensions и генераторы скопированы в Python из Haskellmyrslok
11.01.2018 15:11+1Что-то я не улавливаю, при чем тут лямбды, и совсем не улавливаю, при чем тут Haskell.
Имеется в виду что-то такое:
[x + 1 for x in numbers if x % 2 == 0]
Я утверждаю, что в Python принято писать так, а не с
filter
иmap
(как, кстати, это записать короче с помощью operator?).evocatus
11.01.2018 22:24Лучше использовать по возможности круглые скобочки, чтобы получить generator expression, потому что ваш list comprehension с range(1000000) на Python 3.6 64-bit на Core i5-6500 у меня завис намертво.
from operator import * from functools import partial map(partial(add, 1), filter(lambda x: x%2==0, numbers))
Или так:
from operator import * from functools import partial def inc(x): return x+1 def divisible_by(x): return lambda y: mod(x, y) == 0 map(inc, filter(divisible_by(2), numbers))
Я мог бы сказать, что все эти inc и divisible_by объявляются один раз и выделяются в модуль myshinyfp.py, а потом переиспользуются. Но я даже не буду настаивать на своём, если вы скажете, что это длинно. Но это гибче, изящнее, правильнее.myrslok
12.01.2018 12:15+1Не могу согласиться, что круглые скобочки "лучше", они просто имеют другую семантику. Эти вопросы ортогональны обсуждаемым.
Что касается остального, у вас получилось намного длиннее, так что настаивать на обратном было бы смело. Как я считаю, у вас значительно менее Pythonic. И лямбды как раз у вас, а не у меня. Но уж лучше лямбда, чем
partial(add, 1)
(кстати, было бы интересно попрофилировать, подозреваю это медленнее и лямбды, и comprehension). И совсем нет никаких причин дляmod(x, y)
вместоx % y
.evocatus
12.01.2018 18:20Я прекрасно понимаю разницу между списком и генератором. Просто имел в виду, что если можно использовать генератор, то лучше использовать генератор, потому что его создание «дешевле».
anjensan
12.01.2018 13:20На самом деле не обязательно.
Python 3.6.4 (default, Dec 21 2017, 01:35:12) [GCC 4.9.2] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from timeit import timeit >>> ns = list(range(1000000)) >>> timeit(lambda: sum((x + 1 for x in ns if x % 2 == 0)), number=10) 0.7995037079999747 >>> timeit(lambda: sum([x + 1 for x in ns if x % 2 == 0]), number=10) 0.7783708530000695
Ну раз уж говорим про скорость, варианты с map/filter будут еще медленнее.
>>> from operator import * >>> from functools import partial >>> timeit(lambda: sum(map(partial(add, 1), filter(lambda x: x % 2 == 0, ns))), number=10) 1.7735814190000383
myrslok
12.01.2018 13:43Так я и писал, что с
partial(add, 1)
будет медленнее. Запустите, если не затруднит, еще и так:
timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10)
anjensan
12.01.2018 13:56Я отвечал evocatus. Но вообще partial быстрее чем аналогичная лямбда.
Тут вариант без partial быстрее за счет того, что>>> timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10) 1.7567518400001063
+
(один опкод) быстрее чемadd
(полноценная функция, хоть и написанная на C).
>>> timeit(lambda: sum(map(lambda x: add(x, 1), filter(lambda x: x % 2 == 0, ns))), number=10) 2.1072688089998337
evocatus
12.01.2018 18:18У меня завис вот такой код:
[x + 1 for x in range(1000000) if x % 2 == 0]
anjensan
12.01.2018 18:55Специально для вас не поленился и запустил на своем далеко не самом быстром телефоне (с процом Kirin 650). Печать всего списка заняла на глаз секунд пять. И всего 0.7с если не печатать, а только вычислить.
нотариально заверенный скриншотMikailBag
12.01.2018 22:37А почему печать значений генератора должна быть быстрее?
Единственное преимущество генератора — ленивость. Когда он используется целиком, преимущества сойдут на нет.
veveve
10.01.2018 16:38List comprehensions provide a more concise way to create lists in situations where map() and filter() and/or nested loops would currently be used.
PEP 202
www.python.org/dev/peps/pep-0202
It has been argued that the real problem here is that Python’s lambda notation is too verbose, and that a more concise notation for anonymous functions would make map() more attractive. Personally, I disagree—I find the list comprehension notation much easier to read than the functional notation, especially as the complexity of the expression to be mapped increases. In addition, the list comprehension executes much faster than the solution using map and lambda. This is because calling a lambda function creates a new stack frame while the expression in the list comprehension is evaluated without creating a new stack frame.
Guido van Rossum
python-history.blogspot.ru/2010/06/from-list-comprehensions-to-generator.html
domix32
10.01.2018 14:13У фильтра ровно два параметра который собственно предикат проверки и итерируемый объект. Нет, мы будем писать ручками циклы с условиями и
yield
ить ручками вместоfilter(pred, seq)
. Не то что бы это плохо писать фильтры ручками для понимания, но утверждать, что это упрощает чтение кода определенно не стоит.magic4x Автор
10.01.2018 14:38Да боже мой ) Это пример. Самый простой. Для простого понимания. Вот вам, положите на фильтр:
def foo(seq): if not isinstance(seq, bar) or baz(seq): raise Exception('Bad seq') seen = set() for x in seq: if egg(x) and x not in seen: seen.add(x) yield x
В таком примере слишком много лишнего, чего я не собирался говорить.
MikailBag
10.01.2018 17:19Ну, условно,
if not isinstance(seq, bar) or baz(seq): raise Exception('Bad seq') seq.filter(x).dedup()
magic4x Автор
10.01.2018 19:32У фильтра нет метода
dedup()
и вы вынесли проверку уровнем выше. Т.е. вам придется ее писать всякий раз.MikailBag
10.01.2018 19:35-1Вы написали функцию, и сказали, что ее сложно разложить в композицию простых. Я привел такой пример. Возможно, это ФП, но я не думаю, что это важно. Если считать, что функции высшего порядка — это ФП, то, так или иначе, примерно весь хороший код (благодаря dependency injection, callback, strategy) — функциональный.
magic4x Автор
10.01.2018 20:10Я сказал ровно то, что сказал: автор комментария захотел фильтр + предикат. Я предложил ему генератор, который лучше писать развернуто. Так-то
compose(distinct, partial(filter, pred), validator)
.
myrslok
10.01.2018 15:49Думаю, это просто иллюстрация идеи, потом делается
my_filter = filter
. В неучебном коде, разумеется, не будет нового идентификатора.
Fbist
10.01.2018 10:59Было бы интересно, если бы кто привел результаты работы (количество действий) интерпретатора в зависимости от кода. Уверен, что баловство с функциями притормозит его % на 10. Иногда это критично.
brake
10.01.2018 12:10+1Про Clojure полностью разделяю мнение автора, а про Python нет. Сам был в похожей ситуации, когда «вкусил ФП». Тоже вдохновился и начал выдавать подобные штуки:
list(map(filterfalse(и т.д.)))
— мне казалось, что так красивее и читабельнее. Однако, через пол-годика — годик, когда стал снова копаться в своем коде «функционального периода», я уже был совсем недоволен своими синтаксическими эквилибрами (ведь встроенной композиции в языке нет, а лепить свою или тащить внешние зависимости я даже под эйфорией не хотел).
В итоге все вернул на круги своя: пайтону — пайтоново. Конечно, самое дельное из ФП, вроде избегания глобального состояния, чистые функции, маленькие функции с говорящими именами и т.п. я оставил при себе и использую в Python.
А если хочется ФП и есть возможность — я беру в руки Clojure, программировать на нем не меньшее удовольствие, чем на Python (как будто пазлы разгадываешь).
DarkByte2015
10.01.2018 12:13+1Фишка питона в том что ему не нужно ФП из-за генераторов. Все эти filter и map легко заменяются встроенными в язык генераторами списков/словарей и т.п. И имхо нативный код всегда читается проще чем какие-то функции.
P.S. Как ни странно для ФП лучше подходят C# и Java чем питон. Там хотя-бы есть нормальные стрелочные лямбды типа `x => x * 2`. А в питоне лямбды куда длиннее пишутся, это немного раздражает `lambda x: x * 2`. Зачем спрашивается это дурацкое слово лямбда в начале? Оно только удлинняет код. И без него понятно это лямбда… Да и стрелки вместо двоеточия в лямбдах используются в большинстве языков. Было бы проще если бы он так-сказать следовал традициям.kkirsanov2
11.01.2018 12:02Нет.
Фишка питона в том что у него весьма неудобная композиция в виде декораторов, ограниченая лямбда и неудобный (сравниявая с ML-style ФП) синтаксис для функциональщины.
В результате «функциональный» код на нем смотриться чужеродно.
Тут не зря LINQ вспоминают.
mjr27
10.01.2018 12:41+3Буквально пара придирок по примерам… Ну или мыслей вслух, кому как нравится.
@post_processing(list)
Вообще правила хорошего тона очень уж не рекомендуют декораторам менять тип возвращаемого значения.
- List comprehansion VS functools
У list comprehension есть одна потрясающая особенность: они не прерывают контекст чтения. Одним взглядом сразу становится понятно, что здесь происходит и какой тип у нас на выходе.
Вы пытаететесь утвержать, что
filtered = [x for x in seq if x is not None]
это "куча ненужной фигни" по сравнению с
from operator import is_ from itertools import filterfalse from functools import partial is_none = partial(is_, None) filter_none = partial(filterfalse, is_none) filtered = filter_none(seq)
Серьезно что ли? Посыл понятен, но пример-то свидетельствует о совершенно противоположном
magic4x Автор
10.01.2018 13:21-1Держите:
from mytools import filter_none from itertools import chain filtered = filter_none(seq) filtered2 = filter_none(seq2) all_filtered = filter_none(chain(seq, seq2))
mjr27
10.01.2018 14:33+1Дык все равно
filter_none = lambda seq: (x for x in seq if x is not None)
строчки на три короче и во сколько-то раз читаемей получается
Опять же, повторюсь, посыл понятен. Просто на таких масштабах пример выходит сомнительный. В сложных случаях тоже зачастую обычный
for ... in ...
получается куда более… readable
Очень уж в python LINQ-style
filtered = seq.where(r=> r is not None)
на борту на хватает.
P.S: спасибо за controlcenter :)
magic4x Автор
10.01.2018 15:28Где ж оно короче? ) Давайте считать, берем два массива:
filtered = filter_none(seq) filtered2 = filter_none(seq2) all_filtered = filter_none(chain(seq, seq2)) # versus filtered = (x for x in seq if x is not None) filtered2 = (x for x in seq2 if x is not None) all_filtered = (y for x in (seq, seq2) for y in x if y is not None)
Правда круто бойлерплейта налетает, если сделать хотя бы два раза то же самое?
mjr27
10.01.2018 15:57Считать так считать ))
# v1 from operator import is_ from itertools import filterfalse from functools import partial is_none = partial(is_, None) filter_none = partial(filterfalse, is_none) filtered = filter_none(seq) filtered2 = filter_none(seq2) all_filtered = filter_none(chain(seq, seq2)) # v2 filtered = (x for x in seq if x is not None) filtered2 = (x for x in seq2 if x is not None) all_filtered = (y for x in (seq, seq2) for y in x if y is not None) # v3 from itertools import chain filter_none = lambda seq: (x for x in seq if x is not None) filtered = filter_none(seq) filtered2 = filter_none(seq2) all_filtered1 = filter_none(chain(seq, seq2)) # v4 -- для полных лентяев типа меня def filter_none(seq): return (x for x in seq if x is not None) filtered = filter_none(seq) filtered2 = filter_none(seq2) all_filtered2 = filter_none(seq + seq2)
Какой вариант вызывает минимум WTF в минуту?
magic4x Автор
10.01.2018 16:08Ура, вы написали функцию. Да, предикат повторно использовать не сможете, но сделали же по моему! :) И вот так
(seq + seq2)
не стоит делать. Во-первых, не работает с ленивыми, во-вторых, вы получите третий список/тапл.
evseev
10.01.2018 12:54+1Лично мне импонирует функциональное программирование. Но вот что я не совсем понимаю зачем его притягивать за уши. Ложится что-то прямо сейчас- напиши оставив комментарий. Но зачем это делать везде преодолевая трудности и усложняя жизнь коллег? Хочется ФП? Пиши на Lisp, Haskell и всех их родственниках и потомках. И тебе будет приятно и читать это будут люди, понимающие что происходит.
PS: я за то, что бы любой код, который хотя-бы теоретически может быть прочитан другим человеком, был или самоочевиден или был щедро сдобрен комментариями. Хотя-бы потому, что этим другим человеком можешь быть ты сам, но не выспавшийся, больной или мучимый похмельным синдромом и просто не помнящим что тут вообще происходит.
nick_gabpe
10.01.2018 13:46Скажите, пожалуйста, а почему в первом примере не так?
no_none = filter(None, seq)
magic4x Автор
10.01.2018 13:48Это просто пример, для легкого понимания. И коли это уже второй (третий?) подобный коммент: лично я не фанат использовать
None
в качестве предиката. Потому что он фильтрует не только наны.
random1st
10.01.2018 15:33Лично из моей практики из пакета functools использую partial (можно каррирование делать через лямбды, но выглядит не так хорошо) и wraps для декораторов. Это понятно, относится к стандартной библиотеке и не надо прыгать по коду в поисках какого-нибудь my_filter. C таким подходом в имени функции будет столько же символов, сколько и в ее коде.
amarao
10.01.2018 18:44-2При всём уважении, вот это в продашен-коде в code review я бы завернул к чертям:
def compose(*fns): init, *rest = reversed(fns) return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))
Почему? Вопрос читаемости чуть ниже, а вот ещё один момент: Сигнатура получившегося "очень помогает" интроспекции:
>>> mapv <function compose.<locals>.<lambda> at 0x7f782f1f0400> >>> filterv <function compose.<locals>.<lambda> at 0x7f782f1f0488>
Простите, простите, а что делает filterv?
>>> help(filterv) Help on function <lambda> in module __main__: <lambda> lambda *a, **kw
Ага, смотрим на сигнатуру. Она принимает список аргументов состоящий из позиционных и именованных аргументов. позиционные называются "a", именованные kw.
Соответственно, мы точно можем сказать, что эта функция делает что-то с данными. Очень важное знание.
Но давайте поробуем использовать эту функцию. Внезапно, если у нас в программе определена переменная reduce (локальная переменная!) то код даст потрясающие сайд-эффекты. Почему? Потому что в питоне нет замыканий, а попытка играть в ФЯП без замыканий обречена на унижения.
>>> def reduce(*args): ... print("BAD CODE") ... >>> mapv([], []) BAD CODE
Как же так? Неужели ваша ЧИСТАЯ функция зависит от глобального состояния? Ну куда это годится-то?
А теперь про читаемость. Там всё просто: нечитаемо, перепишите по человечески.
magic4x Автор
10.01.2018 18:56Вы можете написать свой компоуз, который склеивает хелпы и собирает красивую доку, я не прошу пользоваться своей реализацией, она "примерная".
И при всем уважении, я не стал бы работать в команде, где кто-то лезет в чужой модуль, патчит очевидные вещи и потом жалуется на нерабочий код.
amarao
10.01.2018 19:13Я очень извиняюсь, но ничего я не патчу.
Если я пишу вот так вот:
def myfunc(): reduce = True other_func()
То я не ожидаю, что моя локальная переменная повлияет на работу other_func. А вы конструируете лямбду, которая радостно использует локальную переменную reduce вместо функции.
Это называется сайд-эффект и это прямое последствие игрищ с лямбдами вместо нормальных функций.
anjensan
10.01.2018 19:25+1Ваша локальная переменная и не повлияет.
Переменные в интерактивном шелле глобальные (принадлежат модулю '__main__').
Если вы напишете так, как вы показали… То ничего не произойдет.
>>> def prod(s): ... return reduce(lambda x, y: x * y, s) ... >>> def myfunc(): ... reduce = True ... return(prod([1, 2, 3])) ... >>> myfunc() 6
В общем не позорьтесь.amarao
11.01.2018 12:17Ваш пример немного не о том (вы делаете reduce от лямбды, а не возвращаете лямбду с reduce'ом.
Но я тоже совершенно неправ:
def compose(*fns): init, *rest = reversed(fns) return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw)) def no(): reduce=None return compose([],[])() no()
Однако, при этом замыкание не настоящее, если я переопределяю reduce в глобальном пространсте, то он начинает использоваться… Я даже проверил с импортами — сохраняется ссылка на reduce в namespace модуля, где поределена функция.
Если честно, но я не понимаю логики тут. Если reduce берётся в замыкание в момент определения лямбды, то почему её переопределение работает? Если оно не берётся в замыкание, а используется в момент выполнения лямбды, то почему оно берётся из другого namespace'а (не того, в котором выполняется)?
Более того, это какая-то ахинея:
no() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in no File "<stdin>", line 3, in <lambda> NameError: name 'reduce' is not defined import reduce
… И оно принимается.
anjensan
11.01.2018 12:55Однако, при этом замыкание не настоящее
Замыкаются только локальные переменные,reduce
берется из глобал-скоупа.
Если оно не берётся в замыкание, а используется в момент выполнения лямбды, то почему оно берётся из другого namespace'а (не того, в котором выполняется)?
Потому что именно так работает Python. Можете наконец почитать туториал. Или его перевод
Более того, это какая-то ахинея:
В Python3 функциюreduce
убрали из глобальных. Надо ее импортировать изfunctools
.amarao
11.01.2018 13:45Вы знаете, я не поленился почитать, и я там совершенно ничего не увидел про это замечательное «замыкаются только локальные переменные». Что это происходит, я понял, но откуда, кроме как эмирически, это можно понять?
anjensan
11.01.2018 13:57Вы знаете, я не поленился почитать
Перечитайте еще раз.
Вот вам прямые цитаты из приведенной мной ссылки:
В любой момент во время выполнения существует как минимум три вложенных области видимости, чьи пространства имён доступны прямым образом: самая внутренняя[53] область видимости (по ней поиск осуществляется в первую очередь) содержит локальные имена; пространства имён всех объемлющих [данный код] функций, поиск по которым начинается с ближайшей объемлющей [код] области видимости; область видимости среднего уровня, по ней следующей проходит поиск и она содержит глобальные имена текущего модуля; и самая внешняя область видимости (заключительный поиск) — это пространство имён, содержащее встроенные имена.
В вашем примере 'reduce' не объявлена локальной переменной, потому ресолвится в глобальную. Вот тут нету замыкания:
def mr(op): def f(x): return reduce(op, x) return f
А вот тут есть
def mr(op): def f(x): return r(op, x) r = reduce return f
И еще одна цитата
Важно осознавать, что области видимости ограничиваются на текстовом уровне: глобальная область видимости функции, определённая в модуле, является пространством имён этого модуля, независимо от того, откуда или по какому псевдониму была эта функция вызвана.
Внутри функция хранит ссылку на модуль, в котором была объявлена (можно даже сказать, что это замыкание). И неважно, откуда вы потом ее вызываете.amarao
11.01.2018 14:09Мне немножно тяжело с русским, но ок.
Где из процитированного вами сказано, что глобальные переменные в замыкание не попадают?
То есть мой вопрос сейча звучит так: где написано про то, что глобальные имена не попадают в замыкания?
Ещё интереснее вопорс: меня тут в соседнем треде убеждали, что в питоне таки есть замыкания. Так они есть, или их нет?anjensan
11.01.2018 14:32Написано
It is important to realize that scopes are determined textually: the global scope of a function defined in a module is that module’s namespace, no matter from where or by what alias the function is called. On the other hand, the actual search for names is done dynamically...
Поэтому глобальные значения просто не могут захватываться через замыкание (ибо их может просто не существовать на момент объявления функции).
Ещё интереснее вопорс: меня тут в соседнем треде убеждали, что в питоне таки есть замыкания. Так они есть, или их нет?
Более того, именно я утверждал, что замыкания есть.
Болеее того, в сообщении, на которое вы ответили… я тоже писал, что в питоне есть замыания. Вы меня пытаетесь троллить?
Если так, то предлагаю прекратить сею бесцельную дискуссию.
Если нет — искренне прошу, не занимайтесь «code review продашен-кода на Python».
anjensan
10.01.2018 19:00+1Внезапно, если у нас в программе определена переменная reduce (локальная переменная!) то код даст потрясающие сайд-эффекты. Потому что в питоне нет замыканий, а попытка играть в ФЯП без замыканий обречена на унижения.
При всем уважении, но ваши знания Python подхрамывают. Замыкания вполне себе есть, хоть и «read-only» по умолчанию (что исправляется nonlocal).
И даже аттрибут __closure__ у функций есть, что как бы намекает
>>> def f(): x=1; return lambda: x ... >>> f().__closure__ (<cell at 0x7f886ad98558: int object at 0x556d5d395e80>,) >>> f().__closure__[0].cell_contents 1
amarao
10.01.2018 19:16О, оно там есть? Странно, почему тогда мой пример использует локальный reduce, вместо reduce, определённого на момент создания лямбды?
magic4x Автор
10.01.2018 19:29Потому что вы это сделали в том же скопе (консоль, модуль). Ваш пример нежизнеспособен:
reduce = lambda x: None from funcy import compose compose(str, int)(3.2) '3'
datacompboy
Вот так смотришь на синтетику и непонятно «а нафига они нам нужны» (ц) Слоник.
Как с другими задачами — уж больно вы далеки от народа.
Рассказывая о простоте и упрощении, следует подходить с практической позиции, когда для реализации высокоуровневой логики от этого есть прок.
А то вместо ванлайнера (или двулайнера) показывают модуль в десяток строк… проще? r'ly?
И да, композиции тоже надо тестировать. Ни smallcheck ни quickcheck как-то не завезли же.
magic4x Автор
Если написать функцию для удаления нанов и затем ее протестировать, не придется тестировать ее поведение в каждом частном куске кода. Код будет меньше, код будет проще, код будет стабильнее. Функции в отдельности, композированные тоже, конечно нужно тестировать.
Про "далеки от народа" не понял. Хотя могу предположить, что я предлагаю несколько непривычный для python подход — все в порядке, я уже делаю это не в первый раз. Сначала никому не нравится, потом за уши не оторвать. Уж очень красочно выглядит экран функций из одних
compose
.Кстати, это не синтетика, я этим реально пользуюсь в работе. На том же
pluck
(более сложном, конечно же) у меня построен мини DSL для работы со списками и словарями — выкинули кучу кода.random1st
Когда у меня будет падать код, удаляющий наны из списка, я посыплю голову пеплом и пойду копать канавы. Все же читать проще бойлер-плейт, как вы называете, чем вводить кучу новых идентификаторов, тем самым засирая стек. Не отрицаю, возможно в частных случаях это делает код более стройным, но вот весьма спорно то, что это стоит использовать везде.
ser0t0nin
ох не зарекайся насчет пепла и канав
daiver19
Так пишите функцию, кто вам запрещает? Непонятно только зачем все эти функциональные навороты, когда проблема аналогично решается выносом императивно «бойлерплэйта» в отдельную функцию. А еще «pluck» — ужасное имя для функции, лучше бы над именованием задуматься, чем над тем, как извратиться в написании тривиального кода.
deksden
В компьютерных науках есть только две сложные вещи: инвалидация кэша и именование всяких штук )) Второе!
MikailBag
Ну в lodashjs тоже pluck.
werevolff
Немного не понял относительно написания своей функции, которую мы протестируем один раз. Что это за юзкейс? Я пишу утилиту для дропа None из массива. Что я делаю дальше? Пакую её в пакет, выкладывают в сеть и жду, когда её начнут использовать все повсеместно? Скорее-всего, эта функция будет использоваться внутри большого приложения, выполняя простейшую операцию. Но, если часть используемых утилит или встроенных функций, которые задействованы в моих кастомных тулзах поменяют своё поведение после апдейта, мне потребуется всё заново отдебажить, переписать код и тесты. Единственное преимущество такого подхода — я получу более быструю утилиту. Однако, ситуации, когда от её скорости есть толк, будут встречаться пару раз в коде. С другой стороны, если этот код попадёт на обслуживание другому программеру, он будет плеваться. Действительно, какой смысл во всех этих обёртках? Доказать, что автор это может сделать? Что он офигенно крут?
iroln
Если я правильно понял вашу мысль, то может вот это подойдёт: http://hypothesis.works/ ?