Как стать автором
Обновить

Комментарии 88

Вот так смотришь на синтетику и непонятно «а нафига они нам нужны» (ц) Слоник.
Как с другими задачами — уж больно вы далеки от народа.

Рассказывая о простоте и упрощении, следует подходить с практической позиции, когда для реализации высокоуровневой логики от этого есть прок.

А то вместо ванлайнера (или двулайнера) показывают модуль в десяток строк… проще? r'ly?

И да, композиции тоже надо тестировать. Ни smallcheck ни quickcheck как-то не завезли же.

Если написать функцию для удаления нанов и затем ее протестировать, не придется тестировать ее поведение в каждом частном куске кода. Код будет меньше, код будет проще, код будет стабильнее. Функции в отдельности, композированные тоже, конечно нужно тестировать.


Про "далеки от народа" не понял. Хотя могу предположить, что я предлагаю несколько непривычный для python подход — все в порядке, я уже делаю это не в первый раз. Сначала никому не нравится, потом за уши не оторвать. Уж очень красочно выглядит экран функций из одних compose.


Кстати, это не синтетика, я этим реально пользуюсь в работе. На том же pluck (более сложном, конечно же) у меня построен мини DSL для работы со списками и словарями — выкинули кучу кода.

НЛО прилетело и опубликовало эту надпись здесь
ох не зарекайся насчет пепла и канав
Так пишите функцию, кто вам запрещает? Непонятно только зачем все эти функциональные навороты, когда проблема аналогично решается выносом императивно «бойлерплэйта» в отдельную функцию. А еще «pluck» — ужасное имя для функции, лучше бы над именованием задуматься, чем над тем, как извратиться в написании тривиального кода.
В компьютерных науках есть только две сложные вещи: инвалидация кэша и именование всяких штук )) Второе!

Ну в lodashjs тоже pluck.

Немного не понял относительно написания своей функции, которую мы протестируем один раз. Что это за юзкейс? Я пишу утилиту для дропа None из массива. Что я делаю дальше? Пакую её в пакет, выкладывают в сеть и жду, когда её начнут использовать все повсеместно? Скорее-всего, эта функция будет использоваться внутри большого приложения, выполняя простейшую операцию. Но, если часть используемых утилит или встроенных функций, которые задействованы в моих кастомных тулзах поменяют своё поведение после апдейта, мне потребуется всё заново отдебажить, переписать код и тесты. Единственное преимущество такого подхода — я получу более быструю утилиту. Однако, ситуации, когда от её скорости есть толк, будут встречаться пару раз в коде. С другой стороны, если этот код попадёт на обслуживание другому программеру, он будет плеваться. Действительно, какой смысл во всех этих обёртках? Доказать, что автор это может сделать? Что он офигенно крут?

Ни smallcheck ни quickcheck как-то не завезли же.

Если я правильно понял вашу мысль, то может вот это подойдёт: http://hypothesis.works/ ?

Боюсь за такие "упрощения" коллеги меня будут бить. Код становится не читаемым без видимых на то причин. Проще лучше

Дело привычки, поверьте.

Боюсь я был не понят) Люблю фп, но там где он реально уместен. «Упрощать» сomprehensions через создание функции это что-то лишнее.
сomprehensions прочитает любой знакомый с питоном, а чтобы развернуть в голове вот эти функции нужно либо знать все использованные функции высшего порядка, либо все их просмотреть, а значит неоднократно переместиться по коду проекта.
Лучше пусть будет на 2,3,5 строк больше, но чтобы это читалось проще.

Делайте проще:


from mytools import mytool

myresult = mytool(mydata)

Импорт-то прочитать не сложно. Ну и по опыту: в большом проекте это "2-5" строк кода множатся как грибы после дождя, причем бессмысленно.

И все. Никакого лишнего кода.)

Для функциональщиков это может быть и проще, но для императивщиков… Я лично глаза мозги сломал пока разложил всё это мысленно в обычные императивные функции, понял как это работает и понял что лично я так никогда делать не буду :-)
Согласен. Сам стараюсь писать так, чтобы те кто с языком не знакомы могли понимать как код работает. В ущерб самолюбию. Это моя интерпретация догмы «Explicit is better than implicit» — сторониться неочевидного.

«Если долго вглядываться в код, увидишь, что это всего лишь набор символов.» © Я.
НЛО прилетело и опубликовало эту надпись здесь

Я немного слукавил, вы правы, надо было как-то подвести к функциям высшего порядка. Надеюсь, вы не подменяете функции в рантайме?

Lasciate ogni speranza, voi ch’entrate
вот вот, дальше не читал.

Мне кажется, проблема синтаксиса Питона в том, что код типа такого:


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)

Я время от времени порываюсь написать что-то функциональное на питоне, но почти всё время остаётся чувство, что лучше написать решение "в лоб".

Почему Вы противопоставляете 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
В питоне 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 даже портит ее в скопе.
Можно даже чуть короче: filter(None, seq)

Это отфильтрует в том числе 0, False, "", etc.

Для меня идеальный код, это когда его читаешь как текст.
И кстати, зачем в первом примере pred = bool глобальная переменная?
Полагаю автор хотел довести нас к прозрению малыми шагами, а вышло как всегда: кто и без того понимал суть — стали придираться; кому оно не сильно надо — всё равно споткнулись и оставили; наверно, единицы кому зашло с благодарностью улыбнулись и пошли дальше.
Автор молодец, он придумал хорошие примеры, обосновал их полезность, разжевал и отполировал изложение. Кому-то будет полезно наверняка. Я лично почерпнул лишь некоторые тонкости манеры изложения, и оно того стоило, знаете ли. Какой-нибудь пример обязательно пригодится, когда в очередной раз придётся рассказывать молодым коллегам основы.
Извините, но не нужно из python делать clojure (извините, lisp). Да, чистые функции предсказуемы, тестируемы, хорошо параллелятся. Да, функции высшего порядка более универсальны. Да, map reduce легче читается, чем for if for for.
Но все же надо знать меру, и не нужно вводить кучу новых функций ради композиции и каррирования, не нужно более читаемые 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 больше. И дело тут не в размере, а в том, что используя одни и те же инструменты (и понимая как они работают), вам не нужно читать и писать код, который делает то же самое, но конкретно тут и конкретно так каждый раз.


И оно ленивое!

На улице 2018… А всё как 8 лет назад — по основам функционального кода в питоне пишут статьи, а в комментах идет срач, о том что императивный стиль понятнее…
Не понял зачем писать собственные варианты фильтра, когда в области видимости без импортов уже есть `map`, `filter` и `reduce`.

Вероятно для того, чтобы создать самодокументированный код (к чему очень стимулирует, хотя бы, clojure). Типа вместо filter(много параметров), который затрудняет чтение кода, мы, сначала определяем filter_none() и потом его используем как предикат. Читающему глазу уже становится легче.

Все проще: map, filter и reduce питонисты не используют. Я серьезно.

Ну зачем же за всех говорить. :) Вот лично я filter всегда использую, а map — из модуля multiprocessing в виде multiprocessing.Pool().map

Плюс записи вроде «sume_func = lambda x: .....xyz» внутри функций, чтобы ничего лишнего оттуда не выносить «наверх».

Причины не использовать их?

Comprehensions идиоматичнее.

Спорно. К тому же если использовать модуль operator, то map, filter и reduce становятся короче и легче читаются, чем лямбды (у них в Python настолько уродский синтаксис — если хотите поспорить, то посмотрите на синтаксис лямбд в том же Clojure, не говоря уже про haskell, где даже обычные функции объявляются лаконичнее, чем лямбды в Python).
P.S. Напоминаю, что list comprehensions и генераторы скопированы в Python из Haskell

Что-то я не улавливаю, при чем тут лямбды, и совсем не улавливаю, при чем тут Haskell.


Имеется в виду что-то такое:


[x + 1 for x in numbers if x % 2 == 0]

Я утверждаю, что в Python принято писать так, а не с filter и map (как, кстати, это записать короче с помощью operator?).

Лучше использовать по возможности круглые скобочки, чтобы получить 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, а потом переиспользуются. Но я даже не буду настаивать на своём, если вы скажете, что это длинно. Но это гибче, изящнее, правильнее.

Не могу согласиться, что круглые скобочки "лучше", они просто имеют другую семантику. Эти вопросы ортогональны обсуждаемым.


Что касается остального, у вас получилось намного длиннее, так что настаивать на обратном было бы смело. Как я считаю, у вас значительно менее 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)
Я отвечал evocatus. Но вообще partial быстрее чем аналогичная лямбда.

>>> 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

Благодарю.

Специально для вас не поленился и запустил на своем далеко не самом быстром телефоне (с процом Kirin 650). Печать всего списка заняла на глаз секунд пять. И всего 0.7с если не печатать, а только вычислить.
нотариально заверенный скриншот


У вас явно какой-то неправильный питон :)

А почему печать значений генератора должна быть быстрее?
Единственное преимущество генератора — ленивость. Когда он используется целиком, преимущества сойдут на нет.

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) — функциональный.

Я сказал ровно то, что сказал: автор комментария захотел фильтр + предикат. Я предложил ему генератор, который лучше писать развернуто. Так-то compose(distinct, partial(filter, pred), validator).

Думаю, это просто иллюстрация идеи, потом делается my_filter = filter. В неучебном коде, разумеется, не будет нового идентификатора.

Было бы интересно, если бы кто привел результаты работы (количество действий) интерпретатора в зависимости от кода. Уверен, что баловство с функциями притормозит его % на 10. Иногда это критично.

Надеюсь, декоратор вписан в одну строку исключительно для статьи.

Про Clojure полностью разделяю мнение автора, а про Python нет. Сам был в похожей ситуации, когда «вкусил ФП». Тоже вдохновился и начал выдавать подобные штуки: list(map(filterfalse(и т.д.))) — мне казалось, что так красивее и читабельнее. Однако, через пол-годика — годик, когда стал снова копаться в своем коде «функционального периода», я уже был совсем недоволен своими синтаксическими эквилибрами (ведь встроенной композиции в языке нет, а лепить свою или тащить внешние зависимости я даже под эйфорией не хотел).


В итоге все вернул на круги своя: пайтону — пайтоново. Конечно, самое дельное из ФП, вроде избегания глобального состояния, чистые функции, маленькие функции с говорящими именами и т.п. я оставил при себе и использую в Python.


А если хочется ФП и есть возможность — я беру в руки Clojure, программировать на нем не меньшее удовольствие, чем на Python (как будто пазлы разгадываешь).

Фишка питона в том что ему не нужно ФП из-за генераторов. Все эти filter и map легко заменяются встроенными в язык генераторами списков/словарей и т.п. И имхо нативный код всегда читается проще чем какие-то функции.

P.S. Как ни странно для ФП лучше подходят C# и Java чем питон. Там хотя-бы есть нормальные стрелочные лямбды типа `x => x * 2`. А в питоне лямбды куда длиннее пишутся, это немного раздражает `lambda x: x * 2`. Зачем спрашивается это дурацкое слово лямбда в начале? Оно только удлинняет код. И без него понятно это лямбда… Да и стрелки вместо двоеточия в лямбдах используются в большинстве языков. Было бы проще если бы он так-сказать следовал традициям.
Нет.

Фишка питона в том что у него весьма неудобная композиция в виде декораторов, ограниченая лямбда и неудобный (сравниявая с ML-style ФП) синтаксис для функциональщины.

В результате «функциональный» код на нем смотриться чужеродно.
Тут не зря LINQ вспоминают.

Буквально пара придирок по примерам… Ну или мыслей вслух, кому как нравится.


  1. @post_processing(list)
    Вообще правила хорошего тона очень уж не рекомендуют декораторам менять тип возвращаемого значения.


  2. 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 в минуту?

Ура, вы написали функцию. Да, предикат повторно использовать не сможете, но сделали же по моему! :) И вот так (seq + seq2) не стоит делать. Во-первых, не работает с ленивыми, во-вторых, вы получите третий список/тапл.

Лично мне импонирует функциональное программирование. Но вот что я не совсем понимаю зачем его притягивать за уши. Ложится что-то прямо сейчас- напиши оставив комментарий. Но зачем это делать везде преодолевая трудности и усложняя жизнь коллег? Хочется ФП? Пиши на Lisp, Haskell и всех их родственниках и потомках. И тебе будет приятно и читать это будут люди, понимающие что происходит.

PS: я за то, что бы любой код, который хотя-бы теоретически может быть прочитан другим человеком, был или самоочевиден или был щедро сдобрен комментариями. Хотя-бы потому, что этим другим человеком можешь быть ты сам, но не выспавшийся, больной или мучимый похмельным синдромом и просто не помнящим что тут вообще происходит.
Скажите, пожалуйста, а почему в первом примере не так?
no_none = filter(None, seq)

Это просто пример, для легкого понимания. И коли это уже второй (третий?) подобный коммент: лично я не фанат использовать None в качестве предиката. Потому что он фильтрует не только наны.

Это прям «слишком» простое программирование, судя по заглавному изображению.
НЛО прилетело и опубликовало эту надпись здесь

При всём уважении, вот это в продашен-коде в 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

О, оно там есть? Странно, почему тогда мой пример использует локальный reduce, вместо reduce, определённого на момент создания лямбды?

Потому что вы это сделали в том же скопе (консоль, модуль). Ваш пример нежизнеспособен:


reduce = lambda x: None
from funcy import compose
compose(str, int)(3.2)
'3'
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации