Комментарии 88
Как с другими задачами — уж больно вы далеки от народа.
Рассказывая о простоте и упрощении, следует подходить с практической позиции, когда для реализации высокоуровневой логики от этого есть прок.
А то вместо ванлайнера (или двулайнера) показывают модуль в десяток строк… проще? r'ly?
И да, композиции тоже надо тестировать. Ни smallcheck ни quickcheck как-то не завезли же.
Если написать функцию для удаления нанов и затем ее протестировать, не придется тестировать ее поведение в каждом частном куске кода. Код будет меньше, код будет проще, код будет стабильнее. Функции в отдельности, композированные тоже, конечно нужно тестировать.
Про "далеки от народа" не понял. Хотя могу предположить, что я предлагаю несколько непривычный для python подход — все в порядке, я уже делаю это не в первый раз. Сначала никому не нравится, потом за уши не оторвать. Уж очень красочно выглядит экран функций из одних compose
.
Кстати, это не синтетика, я этим реально пользуюсь в работе. На том же pluck
(более сложном, конечно же) у меня построен мини DSL для работы со списками и словарями — выкинули кучу кода.
Немного не понял относительно написания своей функции, которую мы протестируем один раз. Что это за юзкейс? Я пишу утилиту для дропа None из массива. Что я делаю дальше? Пакую её в пакет, выкладывают в сеть и жду, когда её начнут использовать все повсеместно? Скорее-всего, эта функция будет использоваться внутри большого приложения, выполняя простейшую операцию. Но, если часть используемых утилит или встроенных функций, которые задействованы в моих кастомных тулзах поменяют своё поведение после апдейта, мне потребуется всё заново отдебажить, переписать код и тесты. Единственное преимущество такого подхода — я получу более быструю утилиту. Однако, ситуации, когда от её скорости есть толк, будут встречаться пару раз в коде. С другой стороны, если этот код попадёт на обслуживание другому программеру, он будет плеваться. Действительно, какой смысл во всех этих обёртках? Доказать, что автор это может сделать? Что он офигенно крут?
Ни smallcheck ни quickcheck как-то не завезли же.
Если я правильно понял вашу мысль, то может вот это подойдёт: http://hypothesis.works/ ?
Боюсь за такие "упрощения" коллеги меня будут бить. Код становится не читаемым без видимых на то причин. Проще лучше
Дело привычки, поверьте.
сomprehensions прочитает любой знакомый с питоном, а чтобы развернуть в голове вот эти функции нужно либо знать все использованные функции высшего порядка, либо все их просмотреть, а значит неоднократно переместиться по коду проекта.
Лучше пусть будет на 2,3,5 строк больше, но чтобы это читалось проще.
Мне кажется, проблема синтаксиса Питона в том, что код типа такого:
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)
Я время от времени порываюсь написать что-то функциональное на питоне, но почти всё время остаётся чувство, что лучше написать решение "в лоб".
Оно даже в официальном мануале идет в разделе функционального прогаммирования: 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
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 даже портит ее в скопе.
filter(None, seq)
И кстати, зачем в первом примере pred = bool глобальная переменная?
Автор молодец, он придумал хорошие примеры, обосновал их полезность, разжевал и отполировал изложение. Кому-то будет полезно наверняка. Я лично почерпнул лишь некоторые тонкости манеры изложения, и оно того стоило, знаете ли. Какой-нибудь пример обязательно пригодится, когда в очередной раз придётся рассказывать молодым коллегам основы.
Но все же надо знать меру, и не нужно вводить кучу новых функций ради композиции и каррирования, не нужно более читаемые for if for for заменять совершенно нечитаемыми вложенными скобками, не нужно в худших традициях ФП вместо трансформации списков заниматься их копированием.
Возьмите лучшее из обоих миров и спокойно пишите понятный код, не наживая себе хаскель головного мозга, не беспокоясь о том, что где-то не слишком Функционально.
Ну а за попытку из изначально императивного (знаю, мультипарадигменного, но все же) языка сделать чисто функциональный — статье плюс.
Я тоже с трудом вижу где бы такой перефункционализированный подход улучшил питоновский код в обыденной жизни. Однако, как говорится, хорошо подобранным примером можно доказать всё что угодно.
Давайте напряжемся и придумаем за автора (как адвокаты дьявола) пример в его пользу. Это же интересно.
Мне приходит на ум что-то вроде задач сложной настраиваемой обработки потоков данных, когда набор преобразований, применяемых к потоку, требуется сделать кастомизируемым, прозрачным и поддающимся контролю. Я про те самые случаи. когда ООП с его состояниями побочными эффектами плавно превращается в геморрой из фабрик, куч, пуллов, очередей и прочего. Не знаю даже. Подумаю еще=).
Отвечу сразу на два ваших комментария: во-первых, спасибо за лестный отзыв выше, очень непросто написать статью и учесть знания/опыт всей аудитории, и уместить это в размере поста; во-вторых, реальный пример:
@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 больше. И дело тут не в размере, а в том, что используя одни и те же инструменты (и понимая как они работают), вам не нужно читать и писать код, который делает то же самое, но конкретно тут и конкретно так каждый раз.
И оно ленивое!
Вероятно для того, чтобы создать самодокументированный код (к чему очень стимулирует, хотя бы, clojure). Типа вместо filter(много параметров)
, который затрудняет чтение кода, мы, сначала определяем filter_none()
и потом его используем как предикат. Читающему глазу уже становится легче.
Все проще: map, filter и reduce питонисты не используют. Я серьезно.
Плюс записи вроде «sume_func = lambda x: .....xyz» внутри функций, чтобы ничего лишнего оттуда не выносить «наверх».
Причины не использовать их?
Comprehensions идиоматичнее.
P.S. Напоминаю, что list comprehensions и генераторы скопированы в Python из Haskell
Что-то я не улавливаю, при чем тут лямбды, и совсем не улавливаю, при чем тут Haskell.
Имеется в виду что-то такое:
[x + 1 for x in numbers if x % 2 == 0]
Я утверждаю, что в Python принято писать так, а не с filter
и map
(как, кстати, это записать короче с помощью operator?).
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, а потом переиспользуются. Но я даже не буду настаивать на своём, если вы скажете, что это длинно. Но это гибче, изящнее, правильнее.
Не могу согласиться, что круглые скобочки "лучше", они просто имеют другую семантику. Эти вопросы ортогональны обсуждаемым.
Что касается остального, у вас получилось намного длиннее, так что настаивать на обратном было бы смело. Как я считаю, у вас значительно менее Pythonic. И лямбды как раз у вас, а не у меня. Но уж лучше лямбда, чем partial(add, 1)
(кстати, было бы интересно попрофилировать, подозреваю это медленнее и лямбды, и comprehension). И совсем нет никаких причин для mod(x, y)
вместо x % y
.
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
Так я и писал, что с partial(add, 1)
будет медленнее. Запустите, если не затруднит, еще и так:
timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10)
>>> timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10)
1.7567518400001063
Тут вариант без partial быстрее за счет того, что +
(один опкод) быстрее чем add
(полноценная функция, хоть и написанная на C).>>> timeit(lambda: sum(map(lambda x: add(x, 1), filter(lambda x: x % 2 == 0, ns))), number=10)
2.1072688089998337
[x + 1 for x in range(1000000) if x % 2 == 0]
У вас явно какой-то неправильный питон :)
А почему печать значений генератора должна быть быстрее?
Единственное преимущество генератора — ленивость. Когда он используется целиком, преимущества сойдут на нет.
List 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
У фильтра ровно два параметра который собственно предикат проверки и итерируемый объект. Нет, мы будем писать ручками циклы с условиями и yield
ить ручками вместо filter(pred, seq)
. Не то что бы это плохо писать фильтры ручками для понимания, но утверждать, что это упрощает чтение кода определенно не стоит.
Да боже мой ) Это пример. Самый простой. Для простого понимания. Вот вам, положите на фильтр:
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
В таком примере слишком много лишнего, чего я не собирался говорить.
Ну, условно,
if not isinstance(seq, bar) or baz(seq):
raise Exception('Bad seq')
seq.filter(x).dedup()
У фильтра нет метода dedup()
и вы вынесли проверку уровнем выше. Т.е. вам придется ее писать всякий раз.
Вы написали функцию, и сказали, что ее сложно разложить в композицию простых. Я привел такой пример. Возможно, это ФП, но я не думаю, что это важно. Если считать, что функции высшего порядка — это ФП, то, так или иначе, примерно весь хороший код (благодаря dependency injection, callback, strategy) — функциональный.
Думаю, это просто иллюстрация идеи, потом делается my_filter = filter
. В неучебном коде, разумеется, не будет нового идентификатора.
Надеюсь, декоратор вписан в одну строку исключительно для статьи.
Про Clojure полностью разделяю мнение автора, а про Python нет. Сам был в похожей ситуации, когда «вкусил ФП». Тоже вдохновился и начал выдавать подобные штуки: list(map(filterfalse(и т.д.)))
— мне казалось, что так красивее и читабельнее. Однако, через пол-годика — годик, когда стал снова копаться в своем коде «функционального периода», я уже был совсем недоволен своими синтаксическими эквилибрами (ведь встроенной композиции в языке нет, а лепить свою или тащить внешние зависимости я даже под эйфорией не хотел).
В итоге все вернул на круги своя: пайтону — пайтоново. Конечно, самое дельное из ФП, вроде избегания глобального состояния, чистые функции, маленькие функции с говорящими именами и т.п. я оставил при себе и использую в Python.
А если хочется ФП и есть возможность — я беру в руки Clojure, программировать на нем не меньшее удовольствие, чем на Python (как будто пазлы разгадываешь).
P.S. Как ни странно для ФП лучше подходят C# и Java чем питон. Там хотя-бы есть нормальные стрелочные лямбды типа `x => x * 2`. А в питоне лямбды куда длиннее пишутся, это немного раздражает `lambda x: x * 2`. Зачем спрашивается это дурацкое слово лямбда в начале? Оно только удлинняет код. И без него понятно это лямбда… Да и стрелки вместо двоеточия в лямбдах используются в большинстве языков. Было бы проще если бы он так-сказать следовал традициям.
Буквально пара придирок по примерам… Ну или мыслей вслух, кому как нравится.
@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)
Серьезно что ли? Посыл понятен, но пример-то свидетельствует о совершенно противоположном
Держите:
from mytools import filter_none
from itertools import chain
filtered = filter_none(seq)
filtered2 = filter_none(seq2)
all_filtered = filter_none(chain(seq, seq2))
Дык все равно 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 :)
Где ж оно короче? ) Давайте считать, берем два массива:
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)
Правда круто бойлерплейта налетает, если сделать хотя бы два раза то же самое?
Считать так считать ))
# 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 в минуту?
PS: я за то, что бы любой код, который хотя-бы теоретически может быть прочитан другим человеком, был или самоочевиден или был щедро сдобрен комментариями. Хотя-бы потому, что этим другим человеком можешь быть ты сам, но не выспавшийся, больной или мучимый похмельным синдромом и просто не помнящим что тут вообще происходит.
no_none = filter(None, seq)
При всём уважении, вот это в продашен-коде в 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
Как же так? Неужели ваша ЧИСТАЯ функция зависит от глобального состояния? Ну куда это годится-то?
А теперь про читаемость. Там всё просто: нечитаемо, перепишите по человечески.
Вы можете написать свой компоуз, который склеивает хелпы и собирает красивую доку, я не прошу пользоваться своей реализацией, она "примерная".
И при всем уважении, я не стал бы работать в команде, где кто-то лезет в чужой модуль, патчит очевидные вещи и потом жалуется на нерабочий код.
Я очень извиняюсь, но ничего я не патчу.
Если я пишу вот так вот:
def myfunc():
reduce = True
other_func()
То я не ожидаю, что моя локальная переменная повлияет на работу other_func. А вы конструируете лямбду, которая радостно использует локальную переменную reduce вместо функции.
Это называется сайд-эффект и это прямое последствие игрищ с лямбдами вместо нормальных функций.
Переменные в интерактивном шелле глобальные (принадлежат модулю '__main__').
Если вы напишете так, как вы показали… То ничего не произойдет.
>>> def prod(s):
... return reduce(lambda x, y: x * y, s)
...
>>> def myfunc():
... reduce = True
... return(prod([1, 2, 3]))
...
>>> myfunc()
6
В общем не позорьтесь.
Ваш пример немного не о том (вы делаете 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
… И оно принимается.
Однако, при этом замыкание не настоящееЗамыкаются только локальные переменные,
reduce
берется из глобал-скоупа.Если оно не берётся в замыкание, а используется в момент выполнения лямбды, то почему оно берётся из другого namespace'а (не того, в котором выполняется)?Потому что именно так работает Python. Можете наконец почитать туториал. Или его перевод
Более того, это какая-то ахинея:В Python3 функцию
reduce
убрали из глобальных. Надо ее импортировать из functools
.Вы знаете, я не поленился почитатьПеречитайте еще раз.
Вот вам прямые цитаты из приведенной мной ссылки:
В любой момент во время выполнения существует как минимум три вложенных области видимости, чьи пространства имён доступны прямым образом: самая внутренняя[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
И еще одна цитата
Важно осознавать, что области видимости ограничиваются на текстовом уровне: глобальная область видимости функции, определённая в модуле, является пространством имён этого модуля, независимо от того, откуда или по какому псевдониму была эта функция вызвана.Внутри функция хранит ссылку на модуль, в котором была объявлена (можно даже сказать, что это замыкание). И неважно, откуда вы потом ее вызываете.
Где из процитированного вами сказано, что глобальные переменные в замыкание не попадают?
То есть мой вопрос сейча звучит так: где написано про то, что глобальные имена не попадают в замыкания?
Ещё интереснее вопорс: меня тут в соседнем треде убеждали, что в питоне таки есть замыкания. Так они есть, или их нет?
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».
Внезапно, если у нас в программе определена переменная 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
«Простое» программирование на python