Я хочу, чтобы вы задали себе один вопрос и честно на него ответили. Когда в последний раз вы получали настоящее удовольствие от программирования? Оглядываясь назад, я понимаю, что не испытывал подобных ощущений, наверное… уже лет десять. Удовольствия у меня не было ни от JavaScript, ни от Python, ни от Ruby или C — ни от чего. Когда я говорю «удовольствие» — я имею в виду ощущения человека, которого во время работы над неким проектом переполняет искренний восторг. Этот человек постоянно ловит себя на такой мысли: «Ох, ну какая ж круть. Поверить не могу, что моя безумная идея и правда сработала!».

Например, я писал маленькую игру-«рогалик». У меня была такая идея: «Готов поспорить, что у меня получиться воспользоваться этим вашим алгоритмом Дейкстры для соединения комнат при генерировании карты, сначала инвертируя карту, а потом его запуская. Вероятно, мне удастся прокопать отличнейшие туннели между комнатами». То было благословенное время, когда я пытался справиться с этой задачей, и при этом не чувствовал, что C++ мне мешает. Мне тогда удалось решить эту задачу, попутно многому научившись. Потом у меня появилась такая мысль: «Интересно, получится мне взять пользовательский интерфейс, сделанный на FTXUI, и просто напрямую его отрендерить в окно визуализации SFML?». Как и следовало ожидать, у меня всё отлично получилось. И хотя это было не так уж и сложно, я по ходу дела много узнал о том, как в C++ обрабатывается юникод. Ни одна из этих задач лёгкой не была, но все их, в принципе, можно было решить, и я не могу напридумывать себе достаточно много «подводных камней», которыми C++ мог бы помешать мне сделать то, что я хочу. Это — то, что я называю «удовольствием».
Я думаю, что дело тут, в первую очередь, в том, что программисты просто забыли, с какой целью они пришли в программирование. Я знаю, что я учился кодить не для того, чтобы помочь кучке миллиардеров стать ещё богаче. И не для того, чтобы заставить иммутабельный движок рендеринга вывести кнопку василькового цвета. И я, определённо, пришёл в программирование не для того, чтобы выслужиться перед несколькими «диктаторами», правящими каким-нибудь опенсорсным проектом.
Я начал программировать из-за того, что это было здорово и интересно. Помню, как не спал до четырёх утра, отчаянно пытаясь заставить свой отвратный код на gwBASIC вывести символ в консоль MS-DOS. Помню, как целыми неделями работал над странными GUI-идеями и над сетевыми серверами. И делал я это всё только потому, что у меня была какая-то идея. Вот ещё был случай — я на целый месяц ушёл в отладку, чтобы в итоге узнать о том, что причиной ошибки была тупейшая опечатка. И всё это — даже то, что расстраивало, всё это было просто невероятно кайфово! Это было, без сомнения, куда увлекательнее, чем что угодно другое из того, чему я учился.
И я совершенно определённо помню, что и C++ тоже был целым кладезем удовольствий. Даже в те дни, когда вокруг него было слишком много шума, и когда он, если честно, был ужасным языком. Классным он был из-за того, что я, вероятно, мог сделать на C++ всё что угодно. Тогда не надо было ждать, чтобы кто-то, получивший благословение от Python-иллюминатов, сделал бы некую ужасную обёртку вокруг кода проекта, написанного на C++.
Но… где-то пути к нашим дням C++ стал… скучным, растерял свою «кайфовость». И я полагаю, что эта тенденция до сих пор продолжается в отношении всех языков программирования. История «оскучнения» C++ может стать предостережением для других языков. А то, что произошло потом, может дать им надежду на светлое будущее. Я имею в виду великое воскрешение C++, которого никто не заметил.
Шаблонный метапокалипсис C++
С конца 1990-х и до начала 2000-х годов я активно использовал и C++, и Java — до тех пор, пока почти полностью не оставил C++ примерно в то же время, когда C++-сообщество занялось метапрограммированием.

Как бы там ни было, то, что нарисовано выше, отражает мои ощущения по этому поводу. Кучка идиотов нашла новый «углоскобочный» молоток шаблонного метапрограммирования и начала забивать им всё, похожее на гвозди, в радиусе 100 метров. Помню ожесточённые дискуссии с людьми, которые думали, что добавление пары чисел в шаблон было «быстрее», чем добавление тех же чисел прямо в код. И это — при том, что итоговый машинный код получался абсолютно одинаковым в обоих случаях.
Здесь сработали два фактора, которые серьёзно испортили репутацию C++ как в моих глазах, так и в глазах других людей. Первый — это одержимость шаблонным метапрограммированием, которое нашло чрезвычайно широкое применение. Второй — это то, что я воспринимал как странное высокомерие комитета по стандартизации C++, который отказывался сделать язык более удобным для программистов. C++ стал языком для «архитектурных астронавтов», которых больше интересовали идиома pimpl и метапрограммирование, чем забота о том, чтобы языком было бы приятно пользоваться.
Именно поэтому мне тогда и нравился язык Java. Компания Sun Microsystems, определённо, заботилась о программистах, и о том, что им было нужно. В Java был сборщик мусора! Язык Java, конечно, никогда не пойдёт по тому же пути, что и С++. Ни в коем случае. Java-программистов никогда не настигнет одержимость бредовым шаблонным кодом, паттернами AbstractFactoryFactory и Visitor. Они никогда не будут сильнее заботиться о стандартах, чем об удобстве использования языка. Никогда. <кхе-кхе>
.
Полагаю, что это, вероятно, происходило примерно в то же время, когда многие люди моего возраста оставляли C++. Возможно, большинство из вас помнит переусложнённые многословные конструкции этого языка, перегруженные служебными символами вроде угловых скобок, <>
, не дающими особых выгод.
Но C++ продолжил развиваться. Комитет по стандартизации, похоже, понял, что если он что-нибудь не предпримет — C++ превратится в малоизвестный язык, используемый лишь в одной отрасли экономики стоимостью в 10 триллионов долларов. Какая жалость!
Комитет достойно справился со своей задачей, разительно улучшил язык и реально сделал его снова интересным и приятным, не отступив при этом от исходных целей, стоявших перед C++. К сожалению — мало кто знает об этой огромной революции в C++. Поэтому я собираюсь привести ваши представления об этом языке к современному состоянию.
Невероятное возвращение C++ 11
У меня есть к вам просьба: посмотрите на то, что было добавлено в C++ 11. Пройдите по ссылке и взгляните на список потрясающих улучшений, которые появились в этой версии языка. Давайте обратим внимание на несколько самых масштабных изменений:
Ключевое слово
auto
. Да, в C++ действительно появилось ключевое слово для автоматического вывода типов. Я от этого пребывал в таком же шоке, как и вы, так как я помнил о том, что C++ был редкостной сволочью, которая без причины заставляла меня снова и снова вводить одно и то же проклятое слово.Ключевое слово
nullptr
. И правда? Да — комитет исправил сишную гадость, касающуюся странного и загадочного определенияNULL
. Это — и ноль, и не ноль, так как какой-то чудак из комитета по стандартизации C всё ещё пишет код для PDP-11. Там нет нулевых адресов, а слова там имеют длину в 13,5 бит.Цикл
for
, основанный на диапазоне значений. Вы же это и правда читаете? И вы всё прочли правильно — в C++ имеются итераторы и конструкция видаfor(x in list)
, которые есть и в Python. И что самое приятное — всё это работает с ключевым словом auto, поэтому можно перебирать списки, не заботясь о подборе правильного типа. JavaScript всё ещё даже не может себе уяснить то, что такое цикл for, а в C++ всё сделано как надо.Лямбда-выражения. Что? Да, так и есть — в C++ есть лямбда-выражения, и это — не хлам, похожий на то, что присутствует в Python. Лямбда-выражения в C++ удивительно хороши. Я полагаю, что они, в итоге, способны очень сильно изменить подход программистов к проектированию программ. Посмотрите, как их использует FTXUI, чтобы увидеть то, как сильно они повысили удобство использования разных API.
Заголовок
<chrono>
. Вы представляете — что произошло в C++ в 2011 году? В языке появилась библиотека для работы с датой и временем. Её возможности превосходят то, что имеется практически во всех языках, которыми я когда-либо пользовался. Эта библиотека настолько качественно сделана, что можно просто написать 100ms и создать сущность, представляющую 100 миллисекунд. И да — это — корректный типобезопасный синтаксис.Заголовок
<regex>
. И тут нет ошибки — в C++ имеется встроенная библиотека для работы с регулярными выражениями, которая, как и прочие новшества, получилась просто замечательной. Её единственный изъян в том, что она работает не точно так же, как похожий механизм JavaScript в режиме ECMAScript, но в остальном никаких претензий к ней нет.Умные указатели
unique_ptr
иshared_ptr
. С их помощью можно реализовать различные схемы подсчёта ссылок и применять различные практики управления владением и жизненным циклом объектов в памяти. После того, как в языке появились эти указатели, программисты, в основном, перестали массово применять операции по выделению памяти в куче. Теперь они всё чаще отдают предпочтение стеку, и там, где это нужно, используют лишь эти указатели.Заголовок
<thread>
. Да! Многопоточность теперь встроена в сам язык. Что это за язык? Прежний C++, помнится, говорил программистам : «Нужны потоки? Пишите их сами, тупицы». А вот слова нового C++: «Да! Потоки — это очень хорошо. Вот вам потоки».
Я, честно говоря, не знаю о том, что случилось, или о том, почему в C++ 11 появилось так много улучшений. Но то, что произошло с языком, напоминает мне выход стандарта ES6 в мире JavaScript. Это — полный пересмотр философии и стиля языка без отказа от прежних подходов к решению задач.
Да, в C++ это появилось… вроде как
Правда, гораздо важнее то, что теперь C++ — это более современный язык, в котором имеется довольно много такого, что можно найти в других языках. Вот список кое-каких распространённых возможностей, которые могут понадобиться современному программисту:
Надо обойти файловую систему? Обратите внимание на filesystem.
Нужна система управления пакетами? Попробуйте Conan, WrapDB из системы сборки ПО Meson и vcpkg.
Возникла необходимость в матричных вычислениях? Возьмите Eigen.
И, чтоб вы знали, платформа Tensorflow написана на C++.
Вас интересует работа с графикой? В общем — не знаю — надо ли мне упоминать о том, что огромнейшее количество игр написано на C++? Тем, кого интересует графическое применение C++, рекомендую начать с библиотеки SFML.
Требуется создать графический пользовательский интерфейс? Что сказать — могу предположить, что в вашем распоряжении имеются Qt и wxWidgets. И существует ещё одна хорошая библиотека — ImGui. А моя любимая библиотека для создания интерфейсов — это…
Нужен интерфейс для терминала? Полагаю, что, на каком бы языке вы ни писали интерфейсы командной строки, библиотеки, лучше, чем FTXUI, вам не найти. Она замечательно спроектирована. Это — просто замечательный проект. Правда, подходит FTXUI только для терминальных интерфейсов.
В C++ имеется практически всё, что может понадобиться современному программисту. Но, если говорить честно, качество того, что можно найти в экосистеме C++, может порадовать, а может и нет. Я сказал бы, что качество разных C++-инструментов обычно выше, чем можно ожидать, и оно выше, чем качество тех инструментов, которые мне попадаются в сферах JavaScript и Python. Полагаю, если не лукавить — справедливо будет сказать, что нестабильное качество инструментов — это характеристика, применимая к практически каждому из существующих языков. Скажите — вы правда думаете, что система управления пакетами в Python — это первоклассный инструмент? Правда так думаете? А почему тогда существует штук 10 менеджеров пакетов для Python? Вы правда думаете, что хорош абсолютно любой API для GUI? А как насчёт API для работы с файловой системой? Я тут о том, что, если хорошо присмотреться, во всём этом полно недостатков. Поэтому я сказал бы, что в C++ всё почти так же, как в других языках, но большинство инструментов здесь чуть лучше «средней температуры по больнице». Есть здесь и прямо-таки превосходные проекты — вроде FTXUI. И стандартная библиотека C++ тоже очень хороша.
Всё это так, но говорилось об «удовольствии от программирования»
Слышу вас. «Да, конечно, но в Rust имеются реально качественные веб-серверы… и… и — структуры данных, и… что ещё… хорошая стандартная библиотека. Программировать на Rust так же приятно, как и на C++!». Да, но моя точка зрения состоит не в том, что удовольствие от программирования на C++ обеспечивается всеми вышеперечисленными новшествами. Смысл моих высказываний, до сих пор, заключался в следующем:
Вы не правы, если считаете, что С++ — это древний и неповоротливый язык, набитый угловыми скобками, указателями и прочим мусором вроде this->.
Я полагаю, что C++ — это, возможно, самый конкурентоспособный из существующих в наши дни языков, с большим отрывом обходящий другие языки. Не думаю, что мне может встретиться какая-то задача, которую я не смогу решить с помощью C++. Более того — практически всё, что мне может понадобиться, может быть реализовано множеством способов:
Если мне хочется написать настольное приложение — я могу сам написать его код, воспользовавшись чем-то вроде fenster. Ещё я могу прибегнуть к SFML, или использовать QT, или могу олдскульно, без подобных библиотек, написать всё с нуля (взгляните на документацию к fenster — там вы найдёте хорошее руководство по такого рода делам).
Если мне надо воспроизводить звуки — к моим услугам имеется бездна вариантов — от опенсорсных, до платных, включая самые разные «самоделки». Я могу, чтобы наспех что-нибудь соорудить, взять SFML, а могу заплатить за wwise и тогда в моём распоряжении окажется самый лучший инструмент для работы со звуком.
Если мне понадобится работать с 3D-графикой — я могу обратиться к OpenGL, Vulkan, Direct3d, Ogre3d, а так же — ко многим другим подобным проектам. А вообще — 3D-библиотеки для C++ встречаются буквально на каждом шагу.
Если мне нужна будет математика — я смогу найти всё что угодно — от BLAS и Eigen до библиотек вроде GMTL, заточенных под игры.
Необходимо работать с искусственным интеллектом? Напомню, что платформа Tensorflow написана на C++.
Если для решения некоей задачи нет готовой библиотеки — не так сложно напрямую подключиться к почти любому API операционной системы и сделать, по сути, свою библиотеку. Делать такие штуки самостоятельно — это как раз то удовольствие от программирования, о котором я говорю.
И это — лишь несколько примеров. Самое важное — понять, что С++, на самом деле, не мешает программисту. У С++-программиста есть прямой доступ к практически любой библиотеке через ABI C или C++, и прямой доступ к каждой из существующих операционных систем. Поэтому он способен сделать самостоятельно практически всё, что ему может понадобиться.
С++ абсолютно всё равно
Ну ладно, на C++ можно сделать всё, что душе угодно, а комитет по стандартизации C++ в 2011 году откорректировал курс развития языка, нацелив его на производительность труда программистов. И в чём именно тут заключается «удовольствие от программирования», господин Зед А. Шоу?
C++ находится в таком фантастическом творческом «месте силы», где сам язык и его экосистема отличаются исключительно высоким качеством, но сам язык при этом не достаточно популярен для того, чтобы привлечь к нему тех ненормальных, которые поганят языки.
Вы знаете — о ком я говорю. Помните, как я писал о том, что оставил C++ из-за того, что кучка странных людей сходила с ума по паттернам проектирования и по шаблонному метапрограммированию? Потом я пошутил о том, что то же самое случилось с Java — там были паттерны проектирования и безумные XML-файлы. Дальше — то же самое случилось с Ruby — с того момента, как стал популярным фреймворк Ruby on Rails. Этот список пополнили Python, JavaScript и Rust. Каждый раз, когда некий язык становится популярным — на него налетает стая несносных идиотов, которые губят этот язык.
Как видите — C++ сейчас не в моде, поэтому все эти вредители не обращают на него внимания. C++ настолько далёк от какой-бы то ни было фешенебельности, что его ненавидит даже Белый дом. Поэтому все надутые любители командовать, которые вьются вокруг, тыча всех носом в стандарты, перебрались на языки, где могут разбрасываться своей ерундой и инфлюэнсерским мусором.
Это — те самые люди, которые орут: «Почему не пишешь на Rust?!». Они — те, которые стыдят других: «Почему не используешь React?!». Это они надрывают глотку: «Почему так много unsafe в Rust-коде?». Именно им принадлежат такие тирады: «Серьёзно? Цикл for в Ruby? Да чтоб тебя! Неуч!». К нашему счастью — C++ недостаточно популярен для того, чтобы всё это жульё могло бы на нём заработать, поэтому оно его, в основном, игнорирует.
Ну, на самом деле, цепляться к вам будут странные товарищи из стана Rust. Но они обычно так заняты, заботясь о хорошем настроении системы проверки заимствований, что ни на что другое у них времени не хватает, поэтому на них можно просто не обращать внимания.
Всё это значит, что C++ — языку программирования, и сообществу, которое сложилось вокруг C++, просто абсолютно наплевать на то, чем занимается каждый конкретный программист. Хотите писать библиотеку для матричных вычислений? Пишите. Хочется сделать GUI-библиотеку? Вперёд. Появилось желание создать игровой движок? Устройтесь на диване тёмной ночью и действуйте. Придумалось соорудить странную игру, которая наблюдает за вашим кодом и над вами издевается? На самом деле — всем всё равно, и большинству покажется, что это — либо забавно, либо прикольно.
«Абсолютно всё равно» — это важнейшее условие для творчества
«Но Зед, разве нам не нужен Великий блистательный лидер, который скажет — как правильно поступать? Разве то, что в C++ нет Милостивого пожизненного диктатора, не лишает нас надежды найти верный путь? Разве вы не совершаете… осмелюсь заметить… ошибок? Как вы можете выжить без свиты Диктатора, которая вас позорит, заставляя бесплатно на неё работать?»
Я, конечно, не имею в виду, что кто-то мне скажет что-то подобное, но уверен, что у моего читателя крутится в голове что-то на эту тему. Такой настрой — это одна из главных причин, по которой другие языки — это не про удовольствие. Другие языки строго контролируются организациями, которые агрессивно бросаются на всех, кто пытается сделать что-нибудь классное. Если вы пытаетесь сделать проект, конкурирующий с тем, на котором зарабатывает Диктатор — тогда будьте готовы к тому, что вас будут годами, так сказать, «травить собаками».
Я твёрдо верю в то, что для того, чтобы дать расцвести творчеству, нужно иметь возможность заявить о своей идее без страха критики или осуждения. Нужно иметь возможность выразить себя, и уже после того, как идея, кричащая и размахивающая руками, родится, отойти на некоторое расстояние и критически на неё взглянуть. Правда, после того, как идея воплощена, лучше всего, если именно вы станете её первым критиком. Именно этот шаг от «творческой свободы от критики» к «сосредоточенности на качестве» и создаёт условия для выдающегося креативного творчества.
Эту концепцию лучше всего можно объяснить, приведя пример с учителем рисования, который у меня когда-то был. На самом деле, её объясняет поведение всех моих учителей рисования. Они подходили к неоконченной работе и пытались её исправить. Они, что досадно, становились не туда, куда надо. Получается, что они видели работу с высоты в 5 футов, а я её видел с высоты 6 футов и 2 дюйма. Они говорили, что рисунок у меня неправильный, несмотря на то, что перспектива, в которой его видели, совершенно отличалась от моей. Или они комментировали какие-то аспекты неоконченной работы, которые тяжело судить из-за того, что они пока не показаны в составе всего рисунка.
Ошибка, которую допускали эти учителя, заключалась в следующем. Они полагали, что «ошибка», возникшая на раннем этапе, не может быть исправлена в процессе работы, и так и останется «ошибкой» в готовом рисунке. Вполне возможно, что то, что в итоге создаст ученик, окажется, после завершения работы, вполне приемлемым. А пока рисование не окончено — ничто не выглядит правильным, поэтому бессмысленно пытаться это «исправлять».
Есть один важный момент, связанный с творчеством, который не понимали эти учителя. Всё выглядит и воспринимается как ерунда на «уродливой промежуточной» стадии. В самом начале всё кажется просто великолепным, но по мере продвижения работы в неё пробирается беспорядок: чтобы достичь итогового видения идеи, всё в ней перемешивают и перестраивают. Это перемещение от задумки к финалу больше похоже не на вождение автомобиля, а на хождение по канату, когда творец балансирует на тонкой линии, пытаясь достичь цели. Единственный способ научиться ходить по канату — снова и снова падать с него — и так до тех пор, пока учащийся не обретёт равновесие.
Вокруг многих других языков, похоже, сложилась такая ситуация, когда «доброжелатели» постоянно пытаются, на полпути, столкнуть программиста с каната, и продать ему вместо каната автомобиль.
Почти бесконечное пространство для творчества, которое даёт нам C++, в сочетании со сравнительно небольшим количеством «блюстителей чистоты», позорящих других, переполняет творчеством программирование на этом языке. Если у меня есть идея — меня ограничивают лишь мои знания и навыки, а не рамки самого языка и не мнение неких выпендрёжных инфлюэнсеров, которые объявляют, что я неспособен преподавать, делая так из-за того, что мне не нравится их Милостивый пожизненный диктатор.
Справочник cppreference.com — это просто чудо
Думаю — сайт cppreference.com — это пример самой лучшей документации по языку программирования, которой мне когда-либо доводилось пользоваться. Здесь есть практически всё, что, как мне кажется, должно быть в документации по языку. Ниже я скажу о том, чего там не хватает, а пока — вот список того, что мне там нравится:
К каждому ключевому слову и к каждой библиотеке имеются полные справочные материалы.
В статьях документации, посвящённых различным аспектам языка, кроме того, имеются обширные работающие примеры. Я так думаю, что из тех примеров, что я пробовал, лишь около 5% работали не так, как ожидалось. Большая часть этих неувязок произошла не из-за ошибок в тексте документации, а из-за особенностей реализации используемого мной компилятора.
Каждая возможность языка снабжена комментарием по поводу версии стандарта C++, в котором она появилась. Кроме того — в статьях имеются и ссылки на конкретные стандарты, имеющие отношение к той или иной возможности, а так же — на справочные материалы, относящиеся к стандартизации.
На сайте есть строка поиска, основанная на поисковой системе duckduckgo. Поэтому, когда там что-то ищешь — успеху не повредят ни ошибки в написании ключевых слов, ни ситуации, когда не вполне точно помнишь какие-то термины.
В дополнение к полным и понятным справочным материалам, к очень хорошим примерам и к точным сведениям о версиях языка, на сайте имеются подробные описания важных концепций с примерами. Взгляните на справочную статью Copy elision, посвящённую пропуску лишних операций копирования. Эта статья, хотя она и рассчитана на «технарей», написана очень хорошо, и включает в себя множество примеров, иллюстрирующих то, о чём в ней идёт речь.
Единственное, чего не хватает на cppreference.com — это сведений о том, как устанавливать различные компиляторы на разных платформах. Я не могу окончательно для себя решить — нужно или нет администраторам сайта размещать на нём подобные вещи. Для других языков документация такого рода очень важна. Но в случае с C++, полагаю, подобное может столкнуться с недовольством со стороны некоторых из членов сообщества (*кхе-кхе*
Microsoft) и, возможно, поэтому такие сведения на сайт и не добавляют. С другой стороны — учитывая то, что компиляторов C++ и платформ не так уж и много, на cppreference.com вполне можно добавить руководства по началу работы в различных средах.
С++ — это не «праздник каждый день»
Хотя я и испытываю огромное наслаждение, когда пишу на C++, мне хотелось бы, чтобы вы понимали, что я ни в коем случае не думаю, что этот язык совершенно лишён изъянов. Он и близко не может называться идеальным. Я так думаю, что, во-первых, C++ сейчас практически так же хорош, как любой из других существующих языков, и — во вторых — я думаю, что ни один язык не идеален. Свои неувязки есть в C++, и точно так же — есть они и в любом другом языке. Просто проблемы C++ отличаются от проблем других языков.
Например — в C++ крайне тяжело заставить работать в Windows установщик, сделанный не Microsoft. И да — установщик MSYS2 не работает. MSYS2 — это полный отстой, поэтому не надо мне ничего о нём говорить. Мне пришлось создать целую кучу PowerShell-скриптов только для того, чтобы облегчить установку компиляторов и инструментов разработки. Если вы полагаете, что это — просто ужас — тогда почему вы пользуетесь Python? Если вы попытаетесь установить любой Python на Windows — вам придётся иметь дело с тем, что установщик не добавляет Python в PATH… только если это — не благодатный установщик от Microsoft или ActiveState. А таким установщикам позволено добавлять Python в PATH на Windows.
Как видите — тут C++ находится в выигрышном положении, так как нам не приходится разбираться с какими-то «тайнами мадридского двора», которые позволяют Microsoft и ActiveState нарушать явный «закон», запрещающий установщикам Python использовать PATH. Одно только это с моей точки зрения — огромный плюс. И меня не радует необходимость споров с идиотами, которые думают, что это — нормальная ситуация для Python.
Если вы считаете, что ваш язык лучше других — дайте мне что-то около часа, и я найду в нём достаточно гадостей. В свете этих находок ваш язык будет выглядеть как жалкое подобие чего-то такого, что можно назвать «удобным». Проблемы есть во всех языках, дело лишь в том, что недостатки C++ не портят удовольствия от работы с ним.
Вот несколько проблем, с которыми я сталкиваюсь в современном C++:
Более 95% сообщений об ошибках компилятора С++ выглядят гораздо хуже, чем сообщения об ошибках всех остальных языков. И я тут даже не преувеличиваю. Сообщения об ошибках в C++ хуже всех. Полагаю, если комитет по стандартизации языка захочет принести языку огромную пользу — он создаст стандарт для сообщений об ошибках, так как в настоящее время эти сообщения вполне можно назвать «почти вредительскими».
Инструменты для сборки проектов — это просто ужас. Я совершенно не понимаю — почему так тяжело пользоваться инструментами сборки C++-проектов, и почему люди, которые их создают, принимают наиглупейшие решения по поводу этих инструментов. Лучшее, что я до сих пор обнаружил в этой сфере — это сборщик Meson, но и он полон последствий идиотских решений. Например — это игнорирование директив, касающихся установленных пакетов, и использование неисправных системных библиотек без предупреждения. Я наткнулся на эту проблему в день, когда писал эту статью, и меня это просто взбесило.
У производителей компиляторов нет стимула следовать стандартам. Я говорил о том, что в Clang имеется ошибка в
std::source_location
, и это — отличный пример тех проблем, с которыми мы все иногда сталкиваемся. Я твёрдо уверен в том, что комитету по стандартизации языка нужно начать публиковать список компиляторов, в которых не исправлены ошибки, подобные этой. Нужна простенькая страничка, на которой содержится список компиляторов и сведения о том, насколько они соответствуют стандарту. Там же должны быть особо упомянуты компиляторы, в которых имеются очевидные ошибки. Это принесёт огромную пользу.Язык чрезвычайно сложен из-за его истории. В С++ имеется масса устаревших конструкций, через которые приходится продираться для того чтобы найти что-то ценное. У меня возникает такое ощущение, что это похоже на то, как если бы C++ был бы тремя языками (или ещё большим их количеством), похожими на JavaScript. Есть старый стиль, где память практически для всего выделяется в куче, и где почти везде используются обычные указатели. Далее — есть стиль «Шаблонного метапокалипсиса». И имеется ещё стиль «пост-2011», где память почти под всё выделяется в стеке и где очень мало указателей. А ещё я вижу более новый стиль, где все, вместо
class
, используютstruct
, и не особенно часто прибегают к наследованию. Путешествие по всем этим «эрам» C++ любого может сбить с толку, а на YouTube имеется немало знатоков программирования, которые, похоже, застряли в самых разных эпохах языка.Я люто ненавижу RAII. Я встречаюсь с множеством ситуаций, где RAII мешает решать настоящие задачи инициализации сущностей. Причём, мешает настолько, что эта «возможность» C++ становится больше похожей на какой-то сбой, а не на что-то реально полезное. Я считаю (учитывая то, как много других языков этой возможности лишены, никак от этого не страдая), что С++ может пойти на пользу некий промежуточный вариант. Это может быть нечто среднее между текущим подходом, использующим RAII, и тем, где применяются конструкторы инициализации объектов, которые имеются в других языках.
Но всё это — тема для другой статьи. Сейчас мне лишь хотелось бы выразить то, сколько удовольствия мне доставляет написание всяких глупостей в C++, а так же — хотелось бы показать другим людям то, что C++ — это не то, что они себе представляют. Если вы когда-нибудь встречались с учебным курсом, в котором пытаются ругать C++ за обилие угловых скобок — просто знайте, что автор этого курса — круглый невежда, сам не понимающий того, о чём говорит. Да вот, например — посмотрите на этот код разметки пользовательского интерфейса из FTXUI:
document_ = Renderer([&]{
return hbox({
hflow(
vbox(
text(format("HP: {}", player_.hp)) | border,
text(status_text_) | border
) | xflex_grow
),
separator(),
hbox(map_view_->Render()),
});
});
Скажите честно — если бы вы не знали о том, что это — C++, смогли бы вы узнать язык, на котором это написано? Мне больше нечего добавить.
О, а приходите к нам работать? ? ?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Комментарии (191)
DjUmnik
20.01.2025 09:45Кайф был бы, если бы фичи новых стандартов реализовывались вовремя всеми компиляторами. Что там с модулями?
Goron_Dekar
20.01.2025 09:45Нет, вы совсем не правы.
есть только один возможный вариант реализации такого вот сценария - один компилятор, с очень тухлой динамикой релизов. А если их несколько, и они конкурируют, то ждать с релизом и не выкатывать версию, пусть и не включающую реализации всего стандарта - глупость.
Отсутствие всех новых фичей на релизе - это стандарт современного ведения проектов.
dv0ich
20.01.2025 09:45Согласен.
А если бы в плюсы завезли паттерн-матчинг и увезли как можно больше UB - я был бы совсем рад.
Eclipse2339
20.01.2025 09:45(он пожимает плечами)
окэй, попробую убедить фактами. UB позволяет компиляторам генерить более эффективные бинари.
Что будет, если UB убрать из языка?
Если вместо UB при выходе за границы массива принудительно добавить bound checks, код станет на 0.3% менее шустрым (в среднем). Тыц.
Много это или мало? Для HFT, highload сервисов на ограниченных ресурсах и тд и тп - это дофига.
bound checks - лишь один из сотен видов UB. Что будет, если заменить многие из них на безопасную, но избыточную обработку? Это замедлит код не на 0.3%, а гораааздо сильнее.
Но C++ - это в первую очередь про эффективность. Потеряв UB, мы потеряем и в эффективности.
сохранённый ответ
segment
20.01.2025 09:45Скажите честно — если бы вы не знали о том, что это — C++, смогли бы вы узнать язык, на котором это написано? Мне больше нечего добавить.
И зачем так писать код? Как это отлаживать? Потом придет новый программист и будет пробираться через все эти переопределения и лямбды? Со временем начинаешь понимать, что процедурный стиль и легче в отладке и в поддержке.
rukhi7
20.01.2025 09:45интересно что угловые скобки автору вроде как надоели кругом, а вот скобки тела функции внутри другой функции пока видимо нет.
Вообще меня тоже поражает это восхищение возможностью писать функцию внутри функции. Если считается что это лучшая техника, то ее же можно совершенствовать бесконечно - можно написать функцию внутри функции которая внутри третьей функции и так до бесконечности - нет предела совершенству.
vadimr
20.01.2025 09:45Ну вообще это нормальная практика в тех языках, где поддерживаются вложенные функции. Причём она прямо следует из принципа инкапсуляции, хотя обычно причины более технические (связанные с областью видимости).
rukhi7
20.01.2025 09:45Причём она прямо следует из принципа инкапсуляции
то есть когда объявленные локальными переменные в функции улетают непонятно куда с лямбдой это по вашему принцип инкапсуляции? На мой взгляд очень сомнительно.
vadimr
20.01.2025 09:45Конечно, если вы используете лямбду для взлома инкапсуляции, то это нарушение принципа инкапсуляции. Но я отвечал не на это, а на реплику по поводу вложенных функций.
vkni
20.01.2025 09:45когда объявленные локальными переменные в функции улетают непонятно куда с лямбдой это по вашему принцип инкапсуляции?
Нет, это нарушение принципа инкапсуляции, и делает код сложнее. Но если использовать вложенные функции без фанатизма, с должной дисциплиной, это очень удобно. Сами знаете, дай дураку стеклянный ...
IUIUIUIUIUIUIUI
20.01.2025 09:45У меня есть функция
find :: (a -> Bool) -> [a] -> Maybe a
которой передаётся предикат и список, и возвращается первый элемент, удовлетворяющий этому предикату. Я передаю туда лямбду, которая из текущего скоупа захватывает, например, хэшмапу и проверяет вхождение в неё. Где здесь что куда утекает и ломает инкапсуляцию?
vkni
20.01.2025 09:45OFF. Ты знаешь, что можно почитать по применению теории типов в народном хозяйстве?
К примеру, я хочу написать CAD для моделирования электрических схем какого-то сорта, но я хочу сделать 2 уровня моделирования, первый - грубый, чтобы отсекать заведомо негодные схемы до отправки на расчёт. Это - типичная задача для типов. Вопрос - есть ли книжки, где подобное разбирается? Это Domain Driven Design made functional?
То есть, если мы пользуемся Кузошным подходом (framework) - абстрактной интерпретацией, то мы можем электрическую схему рассматривать как программу для расчётного кода, а расчётный код - как интерпретатор. Далее, никто же не запрещает делать более грубый интерпретатор, совместимый с расчётным кодом. Это будет что-то, подобное статической проверке типов.IUIUIUIUIUIUIUI
20.01.2025 09:45К примеру, я хочу написать CAD для моделирования электрических схем какого-то сорта, но я хочу сделать 2 уровня моделирования, первый - грубый, чтобы отсекать заведомо негодные схемы до отправки на расчёт. Это - типичная задача для типов.
Если это не DSL для описания схем, то типы тут не очень помогут.
То есть, ты, конечно, можешь вычислительное ядро своего CAD'а обложить типами, чтобы оно не принимало некорректные задачи, но это всё равно потребует кода по «валидации» пользовательских схем (пусть даже мы валидируем в строго типизированные значения), и никто не помешает описать некорректные пользовательские схемы.
Если же ты хочешь делать DSL, то настоятельно рекомендую почитать algebra-driven design Сэнди Магвайра. Это не совсем про типы, но достаточно близко.
Насчёт непосредственно использования сильных типов в подобных задачах не для академиков я ничего сходу вспомнить не могу. Книжки уровня software foundations — перебор и слишком базовые (это по факту учебник по coq'у, на твой вопрос оно не факт что ответит), условные научные статьи с условного ESOP, как люди там типами проверяют ширину квантовых схем, например — это, ну, научные статьи, а не для народного хозяйства, и люди там придумывают свою очередную систему типов, в хаскеле/идрисе ты такое не запрогаешь (хотя там что-то было про квантовую либу для идриса — могу найти, если интересно).
vkni
20.01.2025 09:45Если это не DSL для описания схем, то типы тут не очень помогут.
Я, видимо, не очень понятно написал - разумеется, DSL. У нас вообще победивший постмодерн в CompSci - всё есть текст, а в компьютере нет ничего, кроме языков и транзисторов.
потребует кода по «валидации» пользовательских схем
Если этот код по валидации правильно написан (см. правила вывода типов Х-М), мы его можем использовать прямо на этапе построения, запрещая делать связи между неподходящими для этого элементами. Гипотетически, запрещая соединять слаботочку и высоковольтную части. Или, как в радиоприёмнике - тракт "постоянного тока" и тракт до выпрямителя.
Проблема в том, что я не очень понимаю, как из этой идеи выжать всё. В смысле, как понять, что можно ещё куда-то двинуться, или же наоборот, там всё украдено до нас.
Если же ты хочешь делать DSL, то настоятельно рекомендую почитать algebra-driven design Сэнди Магвайра. Это не совсем про типы, но достаточно близко.
Спасибо!
хотя там что-то было про квантовую либу для идриса — могу найти, если интересно
Я подозреваю, что это не совсем то.
IUIUIUIUIUIUIUI
20.01.2025 09:45Я, видимо, не очень понятно написал - разумеется, DSL.
Тогда да, ADD Магвайра, и формально доказывать выписанные для комбинаторов законы (и требовать в типах комбинаторов нужный контекст, если оно вылезает из законов).
Проблема в том, что я не очень понимаю, как из этой идеи выжать всё. В смысле, как понять, что можно ещё куда-то двинуться, или же наоборот, там всё украдено до нас.
Попытаться опубликовать статью и посмотреть на реакцию ревьюверов :]
Я подозреваю, что это не совсем то.
ХЗ, но я всё равно нашёл: https://arxiv.org/pdf/2111.10867
Думаю, что оно может оказаться релевантнее, чем кажется сходу.
vkni
20.01.2025 09:45Вообще, я тут понял, что в таких задачах (универсальные расчётные системы вроде CAD) переключение взгляда на "исходные данные = программа" даёт очень многое:
1. Пирамиду:
интерпретация (оно же расчёт) - статический анализ - общий случай абстрактной интерпретации - система типов
2. Можно думать про наши хорошо проработанные методики работы с программами, начиная с VCS.
3. Дальше можно обдумывать, что же означают проекции Футамуры и суперкомпиляция.
rukhi7
20.01.2025 09:45чтобы отсекать заведомо негодные схемы до отправки на расчёт. Это - типичная задача для типов. Вопрос - есть ли книжки, где подобное разбирается?
самая сложная-нетривиальная задача которая относится к типам в таком случае это определиться какие сущности предметной области надо оформлять как типы в программе. В таком случае понятно что книжка такая вряд ли существует так как пришлось бы писать книжку для каждой предметной области. Но можно посмотреть примеры уже существующих систем моделирования, посмотреть какими сущностями они оперируют на пользовательском уровне, на программном уровне( в исходном коде-конечно если есть доступ к исходному коду или хотя бы к какому то SDK для конкретной системы).
vkni
20.01.2025 09:45Я передаю туда лямбду, которая из текущего скоупа захватывает, например, хэшмапу и проверяет вхождение в неё. Где здесь что куда утекает и ломает инкапсуляцию?
Тут ничего не утекает, но "ад коллбеков" тоже ведь существует.
У вас, хаскеллистов, есть более занятная штука, когда замыкания реально утекают, но программа, тем не менее, остаётся вполне обозримой. Эти замыкания торадиционно называются
action
.
Поэтому, на самом деле, при должной культуре программирования даже некоторые утекающие анонимные функции не составляют проблемы. Но работать надо в определённой культуре и поддерживать дисциплину внутри этой культуры.
И, чтобы два раза не вставать, наговню ещё на классы типов - отдельные люди не очень понимают, что классы типов позволяют работать в стиле дурного ООП. В результате, смотришь на код, который вызывает какую-то функцию, но вот без вывода типов хрен поймёшь, какую же именно функцию он вызывает, а из имени совершенно непонятно, что же именно она делает.IUIUIUIUIUIUIUI
20.01.2025 09:45Тут ничего не утекает, но "ад коллбеков" тоже ведь существует.
То, что этим обычно называют — почти синатксическая проблема, что код съезжает сильно вправо.
Я не представляю, что надо делать, чтобы получить ад коллбеков в вызове
find
.В результате, смотришь на код, который вызывает какую-то функцию, но вот без вывода типов хрен поймёшь, какую же именно функцию он вызывает, а из имени совершенно непонятно, что же именно она делает.
Это ж gnuplot!
В правильных классах типов не очень важно, что именно вызывается. Если я дёргаю
length
, то неважно, какой именноFoldable
мне пришёл (с некоторыми условиями типа действительно полиморфного кода).vkni
20.01.2025 09:45Это ж gnuplot!
Ну не до такой степени, но повбывавбы.
В правильных классах типов не очень важно, что именно вызывается. Если я дёргаю
length
Именно. Я, собственно, об этом и пейсал, что нужна культура и дисциплина. А иначе получается аналог ООПшного
BaseObject::build()
, при энтепрайзном дереве наследования от этогоBaseObject
. И в результате, при попытке понять, что же там происходит на prod сервере за DMZ, чувствуешь себя как Кай, у которого лишь кубики с буквами О, П, Ж, А и задание составить слово вечность.
rukhi7
20.01.2025 09:45Где здесь что куда утекает и ломает инкапсуляцию?
вы хотите частным случаем опровергнуть общее утверждение: что лямбды с замыканием можно использовать для нарушения инкапсуляции?
IUIUIUIUIUIUIUI
20.01.2025 09:45Я хочу опровергнуть квантор всеобщности частным случаем (весьма распространённым, к слову), когда выражение под квантором (улетающие байндинги из локального скоупа ведут к нарушению инкапсуляции) не выполняется.
Но можно и проще (для вас): покажите ФП-пример (раз мы тут ФП обсуждаем), где это ломает инкапсуляцию.
rukhi7
20.01.2025 09:45Но можно и проще (для вас): покажите ФП-пример (раз мы тут ФП обсуждаем), где это ломает инкапсуляцию.
начать надо с того что я еще ни разу не видел определения инкапсуляции для ФП, может там и ломать то нечего?
IUIUIUIUIUIUIUI
20.01.2025 09:45Инкапсуляция — это сокрытие иррелевантных деталей реализации. ООП-модификатор
private
— это просто частный случай.rukhi7
20.01.2025 09:45у слова иррелевантный в этом контексте слишком противоречивое значение, что эквивалентно неопределенному значению. Вряд ли на таком определении можно строить какую-то объективную теорию. По сути вы подменили одно умное слово другим умным словом, а это даже как-то не красиво. В таком направлении дискуссия мне не интересна.
IUIUIUIUIUIUIUI
20.01.2025 09:45Потому что у этого термина действительно нет хорошо определённого объективного значения, и проще будет, как это обычно бывает в человеческом языке, оперировать недоопределёнными терминами.
Вряд ли на таком определении можно строить какую-то объективную теорию.
А у нас нет объективной теории ООП, инкапсуляции, и тому подобных вещей. Любые попытки построить такую теорию разбиваются о неприменимость её на практике.
Ну или давайте с другой стороны. Что такое по-вашему инкапсуляция?
По сути вы подменили одно умное слово другим умным словом, а это даже как-то не красиво. В таком направлении дискуссия мне не интересна.
Для протокола отмечу, что вы сами начали разговор с того, что написание в ФП-стиле с передачей лямбд в функции может ломать инкапсуляцию, но теперь при попытках поговорить по существу требуете от других прямо определять этот термин. Почему вам не нужно было его определять, когда вы сказали, что инкапсуляция от этого ломается?
rukhi7
20.01.2025 09:45Что такое по-вашему инкапсуляция?
Инкапсуляция — это сокрытие данных, методов, логики,... любых деталей реализации, которые обеспечивают внешнее поведение объекта в соответствии с конечным типом (типом с которым объект был создан) этого объекта.
Действительно для ФП не подходит так как в ФП нет ни данных, ни методов, ни поведения, ни разделения типов создания и использования, не говоря уже об объектах. Нельзя нарушить то что не определено или просто не рассматривается как необходимое фундаментальное понятие, поэтому я не мог ограничивать себя рамками ФП в своих суждениях.
написание в ФП-стиле
это ваша формулировка для меня она если не совсем некорректна, то очень сомнительна, мне не нужны ограничения ФП, и я не знаю-не понимаю зачем их надо навязывать когда речь заходит о лямбдах.
IUIUIUIUIUIUIUI
20.01.2025 09:45Действительно для ФП не подходит так как в ФП нет ни данных, ни методов, ни поведения, ни разделения типов создания и использования, не говоря уже об объектах.
И логики нет, видимо.
Данные — есть, есть просто ваши ООП-данные и функции.
Методы — это просто функции.
Поведение — это то, что функции делают.
Разделение типов создания и использования — есть (хоть через параметрический полиморфизм, например, хоть через явные преобразования).
Объекты — тоже есть, любые типы.
мне не нужны ограничения ФП
Не нужно легче рассуждать о коде и его поведении? А что вы такое пишете, если не секрет?
rukhi7
20.01.2025 09:45А что вы такое пишете, если не секрет?
так в профиль посмотрите. Пишу что угодно, но конкретно сейчас вот в этой области решения изобретаю: https://github.com/LibreOffice/core
Мне делать нужно. Рассуждать я могу хоть проще, хоть сложнее, это не проблема.
IUIUIUIUIUIUIUI
20.01.2025 09:45Мне делать нужно.
А, ну классическое «некогда думать, делать надо».
Рассуждать я могу хоть проще, хоть сложнее, это не проблема.
Жаль, что на собеседования такие люди не приходят. Мы бы их наняли и, раз для них это не проблема, мигом бы разобрались и с легаси, и с непонятными багами, и с прочим всем.
rukhi7
20.01.2025 09:45А, ну классическое «некогда думать, делать надо».
Передергиваете. Опять не красиво с вашей стороны: где я написал слово некогда?
Если например ваше легаси компилируется 2 часа, времени думать некуда девать даже если всего два раза в день надо скомпилировать. Но лучше конечно подумать до того как начали компилировать, это часто окупается быстрее.
IUIUIUIUIUIUIUI
20.01.2025 09:45Передергиваете. Опять не красиво с вашей стороны: где я написал слово некогда?
Как ещё интерпретировать ваше «мне делать нужно» как очевидно отрицающий мой поинт ответ?
Если например ваше легаси компилируется 2 часа, времени думать некуда девать даже если всего два раза в день надо скомпилировать.
Разговор не про это, а про то, чтобы немножко подумать перед тем, как бросаться писать новый код.
rukhi7
20.01.2025 09:45Разговор не про это, а про то, чтобы немножко подумать перед тем, как бросаться писать новый код.
вы видимо совсем не понимаете что значит код "компилируется 2 часа". А это обычно значит что новый код уже нет смысла писать, надо думать как сократить количество кода, я регулярно и успешно такое делаю в том числе.
vkni
20.01.2025 09:45Комментарии вообще описываются случайными блужданиями. А значит, чем больше размерность, тем менее вероятно, что комменты вернутся к исходной точке. :-)
N_lible
20.01.2025 09:45Согласен с тем что С++ это офигенный ЯП. Изучение программирования начинал на Python, думал на нём же и буду работать. Как мне кажется я его не плохо изучил но никуда не брали а параллельно работал в асу и писал pet проектики на python. Пока по приколу не откликнулся на вакансию С++/qt в своём провинциальном городке и меня с нулевым опытом на С++ взяли "попробовать". Прошло 2 года и я не собираюсь слазить с этого языка, python теперь для чего нибудь быстренького, маленького проектика. А вот С++ нужно закатывать рукава и получать удовольствие. Так что да, С++ это чума!)
vadimr
20.01.2025 09:45Не понял, почему автор считает, что лямбды в C++ лучше, чем в питоне? Хотелось бы какой-нибудь аргумент.
На мой взгляд, в питоне – нормальные классические лямбды из ФП, а в C++ этим словом называется какое-то дитя противоестественной связи ФП и ООП.
Jijiki
20.01.2025 09:45на сколько хватает моих скромных знаний лямда - это анонимный класс на месте вызова лямбды подставляется адрес того класса который вызываем, из нескольких лямбд можно сделать +- нужное навесное поведение, в своё время мне такое ДжиПиТи подсказал я изучил тему и запомнил, тоесть грубо говоря можно квесты лепить на лямбдах тоесть регистрировать в вектор обработчика список квестов и гдето во время исполнения чтото проверять, по-сути лямбда тоже functional программирование тоесть можно и в std::functional засунуть могу пример показать если интересно
class ValidRules { public: int id; std::function<bool()> getRule;//everyRule/everyQuest const char* message; };
bool checkSomethink(CGame<Component<C*>> Somethink,std::vector<int> pT,int c);
//table of quests std::vector< ValidRules > validQuests = { {0, [&]() -> bool { return checkSomethink(Somethink); }, "Message"} };
vadimr
20.01.2025 09:45Всё так и есть в C++. Но в функциональном программировании лямбда – это просто обычное тело функции (анонимное, поскольку [пока] оно не присвоено в качестве значения какому-то имени). Так и в питоне, оператор def для объявления обычной функции является просто синтаксическим сахаром для оператора присваивания, в правой части которого находится лямбда (тело функции).
Класс нужен лямбде в C++ просто по той причине, что в силу используемого в C++ ручного управления памятью лямбде без этого класса (т.е. структуры) негде было бы сохранять свой контекст. То есть это затычка для управления памятью.
Поэтому зачем вообще нужны лямбды, я сам могу прочесть лекцию на несколько часов, а вот почему автор считает, что лямбды в С++ лучше лямбд в питоне – для меня загадка.
Jijiki
20.01.2025 09:45ну например вот я смотрел туториал "Creating a Voxel Engine (like Minecraft) from Scratch in Python" давно еще, там крутые штуки питона используются, но по итогу мощи не хватало и он врубал jit, а по самому питону не скажу
domix32
20.01.2025 09:45а вот почему автор считает, что лямбды в С++ лучше лямбд в питоне – для меня загадка.
мне кажется довольно очевидным, что в питоне писать лямбды сложнее
lamda x: x + 1
смысла нет, ибо код преватится в лапшу заметно быстрее, чем то же произойдёт в С++. Ещё и PEP-ом потом тыкать станут, мол не там пробел, не там запятая, плюс захват контекста имеет свои нюансы.
codev
20.01.2025 09:45Простая причина: многострочность без `\`. Мы пишем лябда-предикаты и пробрасываем в final-объекты функторы НЕ для того, чтобы написать `lambda a: fun(a, global_b)`, а потому что хотим:
облегчить чтение кода, установив определение там, где сущность разово используется,
хотим освободить память от функтора, даже если он захватил состояние из observer-сущности, вместе с завершением {}-блока кода, потому что он выражает весь необходимый алгоритм. Мне кажется, это нормально, когда лямбда может занимать 20 строк, фильтровать коллекцию и рассылать уведомления, если это разовое событие которое не должно длительно храниться в памяти. `[] () {}` (можно сократить) позволяет его оперативно уничтожить в памяти.
zzzzzzerg
20.01.2025 09:45Думаю, что автор имеет в виду проблемы с захватом переменных замыканиям в циклах в Python. В С++ можно выбрать разные режимы захвата, в Python приходится выкручиваться. Ну и надо делать ссылку на авторский стиль.
vadimr
20.01.2025 09:45Так ведь эти режимы захвата придуманы только из-за того, что в C++ нет сборки мусора и надо руками регулировать время жизни объектов. В питоне лямбда образует замыкание со всеми лексически видимыми переменными, так что всё работает автоматически.
zzzzzzerg
20.01.2025 09:45Значит про проблемы вы немного не в курсе.
vadimr
20.01.2025 09:45Возможно. Вот и хотелось бы узнать.
zzzzzzerg
20.01.2025 09:45vadimr
20.01.2025 09:45Ну это известная сложность у людей, изучающих питон после C++, но это не проблема питона и тем более не проблема программирования вообще. Именно так и работает лексическое замыкание в теории и так же оно работает и в питоне. Переменная i здесь относится к главной программе. Если автор вопроса хотел сохранить её значение на момент вызова, то и должен был сделать это (например, заведя формальный параметр i у лямбды).
Точно так же будет работать и обычная, именованная функция. В соответствии с лексической областью видимости имён.
zzzzzzerg
20.01.2025 09:45Вы наверное что-то мне пытаетесь доказать, но я не понимаю зачем. Хотите поспорить - напишите автору (автор в отличие от нас с вами написал свою первую книгу по питону 10+ лет назад).
vadimr
20.01.2025 09:45Я ничего не пытаюсь доказать, я пытаюсь понять, что имел в виду автор в своей статье.
Вы предположили, что он имел в виду случай со stackoverflow, как причину своего утверждения о преимуществе лямбд в C++. Но это нелогично, потому что именно лямбды в питоне работают в данном случае теоретически правильным и ожидаемым непредвзятым пользователем образом. Откуда бы человек вообще стал предполагать, что значение переменной i каким-то образом вдруг зафиксируется, если только он раньше не сталкивался с таким поведением в C++? Общее правило состоит в том, что переменная имеет в каждый момент единственное значение в своей области видимости.
zzzzzzerg
20.01.2025 09:45Они работают ожидаемо, если люди понимают, что такое лексическое замыкание и позднее связывание. У нас на собесах 8 из 10 человек про это не знаю, а код такой писать иногда приходится (чуть сложнее, чем передать индекс в лямбду) и рецепты надо помнить. Ваше высказывание про логично/нелогично приводит меня к мысли, что вы с подобным не сталкивались. Люди вон считали это багами - Issue 13652: Creating lambda functions in a loop has unexpected results when resolving variables used as arguments - Python tracker .
В С++ аналогичной проблемы не будет если лямбда связывается по значениям, а не по ссылкам.
vadimr
20.01.2025 09:45Ваше высказывание про логично/нелогично приводит меня к мысли, что вы с подобным не сталкивались.
Посчитал сейчас статистику по последней программе, за которую получал деньги – 2694 строки, среди них 69 лямбд. Это достаточно для того, чтобы считать, что я сталкивался с лямбдами, или должна быть плотность функционального программирования больше?
позднее связывание
Время связывания вообще не имеет отношения к обсуждаемому вопросу.
У нас на собесах 8 из 10 человек про это не знаю
Охотно верю, но надо просвещать людей.
Люди вон считали это багами
Ну так привыкли к C++.
zzzzzzerg
20.01.2025 09:45Ну посчитайте сколько у вас лямбд внутри цикла.
Позднее связывания к этом относится напрямую.
vadimr
20.01.2025 09:45Да причём здесь связывание? Позднее связывание говорит только о том, что связывание производится по мере обращения к значению. Но само значение переменной i в вашем примере в любом случае к моменту вызова функции выходит за цикл, независимо от того, рано произошло связывание с ним или поздно.
Оттого, что произошло лексическое связывание, само значение же не прекращает меняться во внешней программе.
Связывание и захват значения в C++ – это разные вещи.
Позднее связывание играло бы роль, если бы вы после цикла эту переменную i вообще переопределили другим описанием (хоть это и нетипично для питона).
Например, вот это – позднее связывание:
i = 1 x = lambda : i() def i (): return 2 print (x())
С ранним связыванием получили бы ошибку во второй строке.
А вот в такой программе:
i = 1 x = lambda : i i = 2 print (x())
– наплевать, раннее связывание с i или позднее, всё равно значение i в момент вызова лямбды равно 2, потому что никакой другой переменной i нет.
zzzzzzerg
20.01.2025 09:45Вот вам два примера:
lambdas = [] for idx in range(10): idx_copy = idx lambdas.append(lambda : print(idx_copy)) for l in lambdas: l()
using System; using System.Collections.Generic; namespace HelloWorld { public class Program { public static void Main(string[] args) { var lambdas = new List<Action>(); for (var idx = 0; idx < 10; idx++) { var idx_copy = idx; lambdas.Add(() => Console.WriteLine($"{idx} -> {idx_copy}")); } foreach(var l in lambdas) l(); } } }
Выбрал C# потому что ранее вы говорили про сборку мусора.
vadimr
20.01.2025 09:45Я не знаток C#, но здесь у вас разница не во времени связывания, а в том, что в первом случае у вас используется обычная функциональная лямбда, т.е. просто записанное безымянное тело функции, а во втором – в цикле создаётся новый объект, содержащий функцию. Имеющий в том числе и свою собственную память.
Вот это выражение, как я понимаю, преобразуемое к Action:
() => Console.WriteLine($"{idx} -> {idx_copy}")
– конструктор объекта.
Вот в этом вот фишка:
List<Action>
Сделайте в C# список указателей на функцию, и будет всё так же как в питоне.
Хотя непредвзятому человеку невозможно понять без поллитры, почему idx_copy должно захватываться, а idx нет.
KanuTaH
20.01.2025 09:45Хотя непредвзятому человеку невозможно понять без поллитры, почему idx_copy должно захватываться, а idx нет.
Это-то как раз очевидно: потому что на каждой итерации создаётся отдельный экземпляр
idx_copy
, а вот экземплярidx
один на все итерации.
vadimr
20.01.2025 09:45Понятно. Тут уж моих ничтожных познаний в C# не хватило. Выходит, там динамическая область видимости, а не лексическая?
funny_falcon
20.01.2025 09:45В питоне тоже на каждой итерации создаётся лямбда-объект. Можете легко это проверить с помощью
print(id(l))
во втором цикле.
KanuTaH
20.01.2025 09:45Я не знаток C#, но здесь у вас разница не во времени связывания, а в том, что в первом случае у вас используется обычная функциональная лямбда, т.е. просто записанное безымянное тело функции, а во втором – в цикле создаётся новый объект, содержащий функцию. Имеющий в том числе и свою собственную память.
У вас опять какие-то своеобразные представления о происходящем. В питончике у лямбд тоже вполне себе есть, выражаясь вашим языком, "своя собственная память". Например, что, по-вашему, выведет вот такой код и почему?
def bar(i): return lambda : print(i) def foo(): result = [] for idx in range(10): result.append(bar(idx)) return result lambdas = foo() for l in lambdas: l()
vadimr
20.01.2025 09:45Список 0..9, а в чём подвох? Мы здесь создаём при помощи функции bar 10 разных лямбд, печатающих константы:
lambda : print (0)
lambda: print (1)
...
lambda: print (10)
которые помещаем в элементы списка lamdas при помощи функции foo.
Ни одна из этих лямбд не имеет ни собственных переменных, ни обращается к чужим на этапе применения. Она полностью константная после выполнения return из bar (точнее, после вычисления самого лямбда-выражения, являющегося аргументом return).
KanuTaH
20.01.2025 09:45Мы здесь создаём при помощи функции bar 10 разных лямбд, печатающих константы:
[...]
Ни одна из этих лямбд не имеет ни собственных переменных, ни обращается к чужим на этапе применения.
Здорово вас плющит. Теперь с этой точки зрения объясните, пожалуйста, результат выполнения такого кода:
class Data: i = 0 def __init__(self, i): self.i = i def bar(d): data.append(d) return lambda : print(d.i) def foo(): result = [] for idx in range(10): result.append(bar(Data(idx))) return result data = [] lambdas = foo() for l in lambdas: l() print("---") for d in data: d.i = d.i + 1 for l in lambdas: l()
vadimr
20.01.2025 09:45А здесь в лямбда-выражение входит константное обращение к полю i объекта d. Но поскольку объекты в питоне передаются по ссылке, то константой является сама ссылка, а внутри объекта значение поля можно менять.
Обращайтесь, если что.
KanuTaH
20.01.2025 09:45Какой-то набор слов. В предыдущем случае была якобы (по вашим словам) нагенерирована куча лямбд, печатающих константы 0, 1, и так далее, почему же эта куча лямбд не была нагенерирована в этом случае?
vadimr
20.01.2025 09:45В этом случае тоже нагенерирована куча лямбд. Каждая из них содержит константную ссылку на свой экземпляр класса Data, из которого выбирает поле i. А в первом случае лямбды содержали числа непосредственно, потому что в питоне так устроена передача параметров разных типов.
KanuTaH
20.01.2025 09:45А в первом случае лямбды содержали числа непосредственно, потому что в питоне так устроена передача параметров разных типов.
Бгг, но ведь лямбда не принимает никаких параметров. Все, что она использует в обеих случаях - это одно целое число. Так почему же в первом случае якобы генерируется куча лямбд с константами (типа прямо в коде эти константы якобы прописаны по вашему мнению), а во втором случае - куча лямбд со ссылками? У вас шаблон еще не треснул?
vadimr
20.01.2025 09:45Функция print принимает параметр. И функция bar принимает параметр. И в первом случае это целое число, которое является значением формального параметра i и передаётся по значению, а во втором случае – ссылка на объект d с дальнейшей операцией получения поля объекта. При этом в питоне позднее связывание, и значение выражения операции доступа к полю .i не вычисляется при вычислении лямбда-выражения.
Подставьте вместо .i в явном виде функцию-геттер get_i(), и вам будет понятнее, что происходит.
Поля объекта в питоне не являются просто пассивными смещениями в структуре, как в С++.
KanuTaH
20.01.2025 09:45А вам не приходит в голову более простое объяснение, а именно - никакой отдельный код с константами на каждый случай не генерируется, а вместо этого генерируется "лямбда-класс" с полями-ссылками на захваченные "внешние" объекты (ну, или в случае простейших типов - просто значениями этих объектов), и с методом, код которого генерируется один раз, и который просто работает с соответствующими полями "своего" экземпляра лямбда-класса (как выше написали - "лямбда-объекта"), м?
vadimr
20.01.2025 09:45Нет, не приходит:
1) это не соответствует теории;
2) это фактически неверно. Функциональные значения (в том числе тела именованных фукций и лямбда-выражения) представляет в CPython так называемый code object, который не является объектом в смысле ООП. Хотя у него есть контейнер в виде класса;
3) код в питоне может меняться динамически, в том числе и тип свободных переменных в общем случае неизвестен во время вычисления лямбда-выражения, как я показывал выше. Да и структура классов тоже. Этим я хочу сказать, что класс d может просто-напросто ещё не существовать в момент вычисления лямбды, обращающейся к нему.
KanuTaH
20.01.2025 09:451) это не соответствует теории;
"Если факты не соответствуют теории, то тем хуже для фактов"? :)
2) это фактически неверно. Функциональные значения (в том числе тела именованных фукций и лямбда-выражения) представляет в CPython так называемый code object, который не является объектом в смысле ООП. Хотя у него есть контейнер в виде класса;
Прогоните как-нибудь текст первого моего примера через Cython (генерируемый им код совместим с CPython - по крайней мере, они сами так декларируют: "Cython provides [...] full runtime compatibility with all still-in-use and future versions of CPython") и посмотрите, что там внутри. Разумеется, там нет никакой кодогенерации на лету с кучей констант. Зато там есть примерно такое:
struct __pyx_obj_4test___pyx_scope_struct__bar { PyObject_HEAD PyObject *__pyx_v_i; };
Экземпляр этой структуры создается и заполняется при создании и возврате лямбды, а
print(i)
вызывается из кода лямбды примерно таким образом:__pyx_t_1 = __Pyx_PyObject_CallOneArg(__pyx_builtin_print, __pyx_cur_scope->__pyx_v_i);
Разумеется, никаких констант нет, классический механизм захвата объекта по ссылке через создание структуры, поля которой затем используются для вызова внешних функций внутри лямбды.
vadimr
20.01.2025 09:45Естественно, транслятор с питона на C++ вынужден использовать средства C++.
Как говорил в подобной ситуации Джон Сильвер, “а ты что, ожидал встретить здесь епископа?” Что вы хотели увидеть-то там?
Попробуйте объяснить работу этой программы (не уверен, что она может быть успешно оттранслирована цитоном):
def d(): return 1 l = lambda : d() print (l()) class d: f = 0 print (l()) print (l().f)
Что именно, по-вашему, здесь захватывается в полях лямбда-объекта и как?
И вопрос со звёздочкой: какую семантику имеют скобки в теле лямбды?
KanuTaH
20.01.2025 09:45Что вы хотели увидеть-то там?
Обещанную вами кодогенерацию с кучей констант, вшитых прям в питоновый код же, с JIT'ом каким-нибудь. Вы же говорили, все так работает:
Список 0..9, а в чём подвох? Мы здесь создаём при помощи функции bar 10 разных лямбд, печатающих константы:
lambda : print (0)
lambda: print (1)
...
lambda: print (10)
которые помещаем в элементы списка lamdas при помощи функции foo.
Попробуйте объяснить работу этой программы (не уверен, что она может быть успешно оттранслирована цитоном):
Конечно, может, и успешно им транслируется.
Что именно, по-вашему, здесь захватывается в полях лямбда-объекта и как?
Простите, а зачем тут вообще что-то захватывать? Здесь же все символы в global scope, захватывать здесь ничего не нужно. Все находится через
__Pyx_GetModuleGlobalName()
и сразу выполняется по месту. Посидите самостоятельно, поупражняйтесь, посмотрите, как все работает в реальности, в каких случаях нужен захват (и он делается), а в каких - нет.
vadimr
20.01.2025 09:45Обещанную вами кодогенерацию с кучей констант, вшитых прям в питоновый код же, с JIT'ом каким-нибудь.
В коде на C++, ага?
Вы же говорили, все так работает:
В интерпретаторе так и работает.
Простите, а зачем тут вообще что-то захватывать? Здесь же все символы в global scope, захватывать здесь ничего не нужно. Все находится через
__Pyx_GetModuleGlobalName()
и сразу выполняется по месту.Поймите простую вещь: лямбда не знает, в каком контексте она будет применяться, и какое там будет лексическое окружение. Поэтому она должна сохранять весь используемый ею лексический контекст.
Попробуйте написать так:
def d(): return 1 l = lambda : d() print (l()) class d: f = 0 print (l()) print (l().f) def g(): d = 1 print (l().f) g()
или, наоборот, так:
def h(): def d(): return 1 l = lambda : d() print (l()) class d: f = 0 print (l()) print (l().f) return l d = 0 l = h() d = 1 print (l().f)
Что здесь куда захватывается и когда?
Конечно, может, и успешно им транслируется.
Не забудьте проверить, чтобы результат этой оттранслированной программы соответствовал результату интерпретатора.
KanuTaH
20.01.2025 09:45В коде на C++, ага?
А иначе же "теории не соответствует", ага?
В интерпретаторе так и работает.
Вам уже ниже пытаются объяснить, что и в интерпретаторе "так" не работает. Но вы не хотите посмотреть в отладчик, а продолжаете витать в своих странных схоластических фантазиях.
Поймите простую вещь: лямбда не знает, в каком контексте она будет применяться, и какое там будет лексическое окружение. Поэтому она должна сохранять весь используемый ею лексический контекст.
Если внутри лямбды используются только глобальные переменные, то заниматься сохранением контекста нет смысла - глобальные переменные всегда доступны, они не уничтожатся, если вручную не сделать им
del
. Здесь действительно достаточно только имен переменных, сами соответствующие им объекты сохранять в контекст не нужно. Локальные объекты внутри какой-то функции - дело другое (см. код ниже).или, наоборот, так:
Последний раз показываю. В дальнейшем, пожалуйста, приложите усилия к самостоятельному изучению вопроса. Мне уже надоело заниматься вашим образованием. Вот структура для захвата:
struct __pyx_obj_4test___pyx_scope_struct__h { PyObject_HEAD PyObject *__pyx_v_d; };
Знакомо, да? Вот ее использование в коде лямбды:
__pyx_outer_scope = (struct __pyx_obj_4test___pyx_scope_struct__h *) __Pyx_CyFunction_GetClosure(__pyx_self); __pyx_cur_scope = __pyx_outer_scope; __pyx_t_1 = __Pyx_PyObject_CallNoArg(__pyx_cur_scope->__pyx_v_d);
Это поле в ходе выполнения
h()
переприсваивается несколько раз, последний раз это выглядит так:__pyx_t_1 = __Pyx_Py3ClassCreate(((PyObject*)&PyType_Type), __pyx_n_s_d, __pyx_empty_tuple, __pyx_t_2, NULL, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 10, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_1); __Pyx_GOTREF(__pyx_cur_scope->__pyx_v_d); __Pyx_DECREF_SET(__pyx_cur_scope->__pyx_v_d, __pyx_t_1); __Pyx_GIVEREF(__pyx_t_1); __pyx_t_1 = 0;
vadimr
20.01.2025 09:45Это поле в ходе выполнения
h()
переприсваивается несколько разОчень хорошо. А теперь напоминаю, с каким вашим высказыванием я спорю:
генерируется "лямбда-класс" с полями-ссылками на захваченные "внешние" объекты (ну, или в случае простейших типов - просто значениями этих объектов), и с методом, код которого генерируется один раз, и который просто работает с соответствующими полями "своего" экземпляра лямбда-класса (как выше написали - "лямбда-объекта")
Надеюсь, вы видите, что по ходу выполнения h() нет никаких обращений к лямбде в исходном тексте?
А то, что у среды исполнения есть свои собственные структуры для хранения данных о состоянии программы - бесспорно.
Если не путать семантику конструкций питона с устройством реализующего их кода на языке низкого уровня, то всё становится на свои места.
Если внутри лямбды используются только глобальные переменные, то заниматься сохранением контекста нет смысла - глобальные переменные всегда доступны, они не уничтожатся, если вручную не сделать им
del
. Здесь действительно достаточно только имен переменных, сами соответствующие им объекты сохранять в контекст не нужно. Локальные объекты внутри какой-то функции - дело другоеЛюбой объект, на который есть актуальные ссылки (в том числе из лямбды) не уничтожается в силу принципа сборки мусора. Неважно, глобальная это переменная или нет. У нас есть ссылка на имя, а имя ссылается на значение.
KanuTaH
20.01.2025 09:45Надеюсь, вы видите, что по ходу выполнения h() нет никаких обращений к лямбде в исходном тексте?
Надеюсь, вы видите, что Cython не генерирует плюсовый код? Ах да, не видите, вы же рассчитываете, что другие вам все расскажут и покажут, а вы будете только задавать глупые вопросы с умным видом. Так вот, Cython генерирует код на C. Там нет классов. Лямбда - это обычная функция, которая принимает параметр
__pyx_self
, который является грубым аналогом плюсовогоthis
(который так же первым параметром неявно принимают в плюсах нестатические методы классов). Через него и можно добраться до контекста/замыкания, как я показал выше, контекст неразрывно связан с конкретным экземпляром "лямбда-объекта", на который собственно и указывает этот__pyx_self
.А то, что у среды исполнения есть свои собственные структуры для хранения данных о состоянии программы - бесспорно.
Как говорится, хоть стрижено, хоть брито - все голо.
vadimr
20.01.2025 09:45Так вот, Cython генерирует код на C.
Да какая разница-то? Вы самомодификацию кода на C++ или на C ожидали увидеть?
Лямбда - это обычная функция,
Ну слава богу, наконец!
как я показал выше, контекст неразрывно связан с конкретным экземпляром "лямбда-объекта"
Как же он неразрывно связан, если, по вашим собственным наблюдениям, к лямбде никаких обращений нет, а контекст меняется?
Лямбда просто находится в определённом контексте, и с точки зрения денотационной семантики, и с точки зрения операционной (устройства среды исполнения). Но этот контекст не является частью лямбды, как конструкции языка. И тем более как объекта в смысле ООП, так как не всегда связан с использованием этого объекта.
KanuTaH
20.01.2025 09:45Как же он неразрывно связан, если, по вашим собственным наблюдениям, к лямбде никаких обращений нет, а контекст меняется?
А какие должны быть "обращения к лямбде" в C коде, вызов функции что ли? А зачем? А вот обновление её контекста по ходу его изменения, разумеется, производится. Я не просто так акцентировал внимание на том, что генерируемый код - не плюсовый, и понятие "класс" там отсутствует. Производится просто эмуляция "лямбда-объекта" с использованием негустого набора доступных в C средств, вот и все. Забавно, что приходится разжевывать такие элементарные вещи.
Но этот контекст не является частью лямбды, как конструкции языка.
Просто конкретно в C нет средств сделать контекст "частью лямбды" в вашем представлении - с помощью конструкций языка. Но тем не менее даже там у лямбда-объекта есть своя "память" в виде структурки с набором захваченных внешних по отношению к нему объектов. А как это выглядит со стороны питона - вам уже написали ниже, как и куда смотреть. Засим откланиваюсь, удачи в дальнейшем образовании, всего хорошего.
vadimr
20.01.2025 09:45А какие должны быть "обращения к лямбде" в C коде
Не в C коде, а в питоновском. Контекст лямбды у вас меняется без упоминания в питоновском исходнике этой лямбды. Объект так не работает.
Я не просто так акцентировал внимание на том, что генерируемый код - не плюсовый, и понятие "класс" там отсутствует. Производится просто эмуляция "лямбда-объекта" с использованием негустого набора доступных в C средств, вот и все. Забавно, что приходится разжевывать такие элементарные вещи.
Я не знаю, зачем вы это разжёвываете, потому что это понятно. Но мы же говорим о питоновских классах, а не плюсовых или их сишной эмуляции.
funny_falcon
20.01.2025 09:45У списка нет элементов, окститесь. Список - это массив с поинтерами на объекты. Можете это проверить, сохранив один и тот же объект в список (или даже в разные списки), и так же на итерации позвав id от «элемента»
Подсказка: замыкание где? В «контейнере», или в code object?
vadimr
20.01.2025 09:45Замыкание, конечно, в code object, но это никак не относится к обсуждаемому вопросу.
А что касается id, то Вы правы.
zzzzzzerg
20.01.2025 09:45Фишка не в
List<Action>
, а в том что создается функциональный объект, со своей памятью. Впрочем так же происходит и в случае питона - там также создается функциональный объект со своей памятью. В моем примере можете добавить выводprint(id(l))
чтобы в этом убедиться. Либо сделатьdis
и также убедиться что вызываетсяMAKE_FUNCTION
.Если под отладчиком посмотрите содержимое лямбды (l), то там можете увидеть
__closure__
, который содержит ссылки на свободные (захваченные) переменные (в нашем случае только одну ссылкуidx_copy
), а в__code__.co_freevars
названия захваченных переменных. И если посмотреть наid
переменных, которые лежат в__closure__
, то можно заметить, что все они соответствуют последней копииidx_copy
.Я немного модифицировал пример - добавил две переменные в замыкание, чтобы было проще смотреть, что попадает в захваченные переменные.
def create(): lambdas = [] idx_copy = 0 for idx in range(10): idx_copy = idx + 1 print ('-', id(idx), id(idx_copy)) lambdas.append(lambda : print(idx_copy, idx)) return lambdas import dis dis.dis(create) def test(): for l in create(): l() print (id(l), [(c.cell_contents, id(c.cell_contents)) for c in l.__closure__], l.__code__.co_freevars) test()
А вот так можно сделать небольшую ошибку и сломать свою лямбду:
def create(): lambdas = [] idx_copy = 0 for idx in range(10): idx_copy = idx + 1 print ('-', id(idx), id(idx_copy)) lambdas.append(lambda : print(idx_copy, idx)) del idx_copy return lambdas
Причем здесь указатели на функции я простите не понял. Мы все таки вели разговор про замыкания.
Хотя непредвзятому человеку невозможно понять без поллитры, почему idx_copy должно захватываться, а idx нет.
Там оба захватываются - полно онлайн компиляторов - можно и посмотреть.
vadimr
20.01.2025 09:45Лексическое замыкание, конечно, существует в питоне. Но это не значит, что захватываются какие-либо значения в лямбда-объекте. Лексическое замыкание означает просто сохранение лексического контекста имён свободных переменных. Сохраняются имена, а не значения.
Что касается C#, то, насколько я понимаю, функциональный объект создаётся именно приведением лямбда-выражения к объектному типу Action. Хотя настаивать не буду, так как этот язык мне неизвестен.
zzzzzzerg
20.01.2025 09:45Удивительно, что вы не хотите посмотреть как реальность соответствует теории и продолжаете заблуждаться. Не верите отладчику и моим словам - вот вам немного ссылок:
Как вы сказали выше "Охотно верю, но надо просвещать людей." - просвещайтесь пожалуйста.
По поводу вашего вопроса вам уже ответили - там глобальный скоп - там нет свободных переменных и они не захватываются -
__code__.co_names
иfunction.__globals__
вам в помощь.Что касается C#, то, насколько я понимаю, функциональный объект создаётся именно приведением лямбда-выражения к объектному типу Action.
Вы неправильно понимаете. Почитать можно тут Lambda expressions - Lambda expressions and anonymous functions - C# reference | Microsoft Learn , тут Built-in reference types - C# reference | Microsoft Learn и тут https://www.codeproject.com/KB/cs/InsideAnonymousMethods.aspx
vadimr
20.01.2025 09:45Вы сами-то читаете тексты, ссылки на которые даёте?
Вы привели ссылки на то, что представляет собой и как организовано лексическое окружение в питоне. Что вы хотели этим доказать?
Что касается C#.
Вы неправильно понимаете. Почитать можно тут Lambda expressions - Lambda expressions and anonymous functions - C# reference | Microsoft Learn
Ну и тут прямо написано:
You use a lambda expression to create an anonymous function.
...
Any lambda expression can be converted to a delegate type. The types of its parameters and return value define the delegate type to which a lambda expression can be converted. If a lambda expression doesn't return a value, it can be converted to one of the
Action
delegate types; otherwise, it can be converted to one of theFunc
delegate types.То есть именно то, что я написал: лямбда-выражение – это функция; её можно преобразовать в значение (экземпляр) типа (класса) Action. Что и делает ваш List<Action>.
И по выделенной ссылке:
A
delegate
is a reference type that can be used to encapsulate a named or an anonymous method.То есть сама по себе лямбда объектом в смысле ООП не является, представляя собой просто исполняемый код, интерпретируемый в своём лексическом окружении; но она может быть инкапсулирована в объект класса Action или Func. Это в точности именно то, о чём я пишу всё это время, и с чем (как я понял) Вы и ваш товарищ спорите.
zzzzzzerg
20.01.2025 09:45Вы привели ссылки на то, что представляет собой и как организовано лексическое окружение в питоне. Что вы хотели этим доказать?
Ну вообще то там написано, что замыкания захватывают значения свободных переменных. И этим я хочу опровергнуть ваше утверждение "Лексическое замыкание означает просто сохранение лексического контекста имён свободных переменных. Сохраняются имена, а не значения.". Вы могли бы и сами это узнать, если бы прочитали ссылки, которые я привел.
То есть именно то, что я написал: лямбда-выражение – это функция; её можно преобразовать в значение (экземпляр) типа (класса) Action. Что и делает ваш List<Action>.
лямбда-выражение не функция. Вы могли бы с этим ознакомиться, если бы читали более внимательно и прочитали бы, например, последнюю ссылку (довольно объемную, но для вас там есть оглавление).
но она может быть инкапсулирована в объект класса Action или Func. Это в точности именно то, о чём я пишу всё это время, и с чем (как я понял) Вы и ваш товарищ спорите.
Вы неумело жонглируете словами, с этим я, и мой коллега по опасному бизнесу, и спорим.
Ниже IL (intermediate language) кода на C# из примера выше. Как видно ни
<>c__DisplayClass0_0
ни<>c__DisplayClass0_1
не расширяютAction
, но являются функциональными объектами..class private auto ansi '<Module>' { } // end of class <Module> .class public auto ansi beforefieldinit HelloWorld.Program extends [System.Runtime]System.Object { // Nested Types .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0' extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Fields .field public int32 idx // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x210c // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method '<>c__DisplayClass0_0'::.ctor } // end of class <>c__DisplayClass0_0 .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_1' extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Fields .field public int32 idx_copy .field public class HelloWorld.Program/'<>c__DisplayClass0_0' 'CS$<>8__locals1' // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x210c // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method '<>c__DisplayClass0_1'::.ctor .method assembly hidebysig instance void '<Main>b__0' () cil managed { // Method begins at RVA 0x2118 // Code size 69 (0x45) .maxstack 3 .locals init ( [0] valuetype [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler ) IL_0000: ldloca.s 0 IL_0002: ldc.i4.4 IL_0003: ldc.i4.2 IL_0004: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::.ctor(int32, int32) IL_0009: ldloca.s 0 IL_000b: ldarg.0 IL_000c: ldfld class HelloWorld.Program/'<>c__DisplayClass0_0' HelloWorld.Program/'<>c__DisplayClass0_1'::'CS$<>8__locals1' IL_0011: ldfld int32 HelloWorld.Program/'<>c__DisplayClass0_0'::idx IL_0016: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0) IL_001b: nop IL_001c: ldloca.s 0 IL_001e: ldstr " -> " IL_0023: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string) IL_0028: nop IL_0029: ldloca.s 0 IL_002b: ldarg.0 IL_002c: ldfld int32 HelloWorld.Program/'<>c__DisplayClass0_1'::idx_copy IL_0031: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0) IL_0036: nop IL_0037: ldloca.s 0 IL_0039: call instance string [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::ToStringAndClear() IL_003e: call void [System.Console]System.Console::WriteLine(string) IL_0043: nop IL_0044: ret } // end of method '<>c__DisplayClass0_1'::'<Main>b__0' } // end of class <>c__DisplayClass0_1 // Methods .method public hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2050 // Code size 160 (0xa0) .maxstack 3 .locals init ( [0] class [System.Collections]System.Collections.Generic.List`1<class [System.Runtime]System.Action>, [1] class HelloWorld.Program/'<>c__DisplayClass0_0', [2] class HelloWorld.Program/'<>c__DisplayClass0_1', [3] int32, [4] bool, [5] valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<class [System.Runtime]System.Action>, [6] class [System.Runtime]System.Action ) IL_0000: nop IL_0001: newobj instance void class [System.Collections]System.Collections.Generic.List`1<class [System.Runtime]System.Action>::.ctor() IL_0006: stloc.0 IL_0007: newobj instance void HelloWorld.Program/'<>c__DisplayClass0_0'::.ctor() IL_000c: stloc.1 IL_000d: ldloc.1 IL_000e: ldc.i4.0 IL_000f: stfld int32 HelloWorld.Program/'<>c__DisplayClass0_0'::idx IL_0014: br.s IL_0059 // loop start (head: IL_0059) IL_0016: newobj instance void HelloWorld.Program/'<>c__DisplayClass0_1'::.ctor() IL_001b: stloc.2 IL_001c: ldloc.2 IL_001d: ldloc.1 IL_001e: stfld class HelloWorld.Program/'<>c__DisplayClass0_0' HelloWorld.Program/'<>c__DisplayClass0_1'::'CS$<>8__locals1' IL_0023: nop IL_0024: ldloc.2 IL_0025: ldloc.2 IL_0026: ldfld class HelloWorld.Program/'<>c__DisplayClass0_0' HelloWorld.Program/'<>c__DisplayClass0_1'::'CS$<>8__locals1' IL_002b: ldfld int32 HelloWorld.Program/'<>c__DisplayClass0_0'::idx IL_0030: stfld int32 HelloWorld.Program/'<>c__DisplayClass0_1'::idx_copy IL_0035: ldloc.0 IL_0036: ldloc.2 IL_0037: ldftn instance void HelloWorld.Program/'<>c__DisplayClass0_1'::'<Main>b__0'() IL_003d: newobj instance void [System.Runtime]System.Action::.ctor(object, native int) IL_0042: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<class [System.Runtime]System.Action>::Add(!0) IL_0047: nop IL_0048: nop IL_0049: ldloc.1 IL_004a: ldfld int32 HelloWorld.Program/'<>c__DisplayClass0_0'::idx IL_004f: stloc.3 IL_0050: ldloc.1 IL_0051: ldloc.3 IL_0052: ldc.i4.1 IL_0053: add IL_0054: stfld int32 HelloWorld.Program/'<>c__DisplayClass0_0'::idx IL_0059: ldloc.1 IL_005a: ldfld int32 HelloWorld.Program/'<>c__DisplayClass0_0'::idx IL_005f: ldc.i4.s 10 IL_0061: clt IL_0063: stloc.s 4 IL_0065: ldloc.s 4 IL_0067: brtrue.s IL_0016 // end loop IL_0069: nop IL_006a: ldloc.0 IL_006b: callvirt instance valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<!0> class [System.Collections]System.Collections.Generic.List`1<class [System.Runtime]System.Action>::GetEnumerator() IL_0070: stloc.s 5 .try { IL_0072: br.s IL_0085 // loop start (head: IL_0085) IL_0074: ldloca.s 5 IL_0076: call instance !0 valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<class [System.Runtime]System.Action>::get_Current() IL_007b: stloc.s 6 IL_007d: ldloc.s 6 IL_007f: callvirt instance void [System.Runtime]System.Action::Invoke() IL_0084: nop IL_0085: ldloca.s 5 IL_0087: call instance bool valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<class [System.Runtime]System.Action>::MoveNext() IL_008c: brtrue.s IL_0074 // end loop IL_008e: leave.s IL_009f } // end .try finally { IL_0090: ldloca.s 5 IL_0092: constrained. valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<class [System.Runtime]System.Action> IL_0098: callvirt instance void [System.Runtime]System.IDisposable::Dispose() IL_009d: nop IL_009e: endfinally } // end handler IL_009f: ret } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x210c // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class HelloWorld.Program
Впрочем, продолжать с вами обсуждение я смысла не вижу - вы либо не читаете ссылки, которые вам дают, либо понимаете их по своему - тут я уже ничего поделать не могу. Просвещайтесь.
vadimr
20.01.2025 09:45Ну вообще то там написано, что замыкания захватывают значения свободных переменных.
Где именно такое написано??? Можно конкретную цитату?
Они сами свободные переменные захватывают, а не их значения.
лямбда-выражение не функция. Вы могли бы с этим ознакомиться, если бы читали более внимательно и прочитали бы, например, последнюю ссылку (довольно объемную, но для вас там есть оглавление).
Можно конкретную цитату? Строго говоря, конечно, функция – это именованное лямбда-выражение.
Ну и дальше, какое отношение ваша простыня на ассемблере ВМ имеет к семантике языка питон или С#? Естественно, что, когда вы дойдёте до машинного кода, там вообще останутся одни байты. Мы говорим о семантике конструкции языка, а вы зачем-то приводите реализацию. В реализации разницы вполне может не быть (как, например, между целым и булевским значением в машинном коде), но в семантике она есть.
То же и ваш коллега по опасному бизнесу. Сам своими руками в своей простыне второй футамуровской проекции нашёл реализацию контекста в питоне. Нашёл, где и как этот контекст изменяется при продвижении выполнения по тексту программы (лексической области видимости). Всё верно нашёл, целеустремлённо. И после этого сам пишет, что лексический контекст является полем лямбды, хотя сам же убедился, что он находится снаружи её и общий для всех операторов лексической области видимости, они его изменяют друг за дружкой.
Прикладные навыки без фундаментальных знаний. Горе от ума прямо.
zzzzzzerg
20.01.2025 09:45Понятно, что они захватывают объекты, которые эти значения хранят. Но вы ранее утверждали, что
захватываютсясохраняются только имена. Поэтому я вам и привел эту ссылку и предлагал посмотреть, что там в замыкании реально захватывается.Можно конкретную цитату? Строго говоря, конечно, функция – это именованное лямбда-выражение.
Сами уж поищите, вы ловко виляете - понятно, что в контексте нашего разговора мы говорим не просто о функции, а о функции и захваченном контексте.
Простыня на IL для того чтобы вы посмотрели, что контекст захвачен, а Action-ном там не пахнет. Но опять же вас судя по всему интересуют теоретические витания, чем реальность данная нам в ощущениях.
vadimr
20.01.2025 09:45Понятно, что они захватывают объекты, которые эти значения хранят. Но вы ранее утверждали, что
захватываютсясохраняются только имена. Поэтому я вам и привел эту ссылку и предлагал посмотреть, что там в замыкании реально захватывается.А я вам привёл небольшие и вполне понятные тексты программ, из запуска которых видно, что в питоне захватываются именно имена (в своём лексическом контексте), а объекты и даже типы, соответствующие этим именам, вы можете менять в любой момент, переопределяя имена, и лямбда всё это съест.
Сами уж поищите, вы ловко виляете - понятно, что в контексте нашего разговора мы говорим не просто о функции, а о функции и захваченном контексте.
Я вас, может быть, удивлю, но любая функция в питоне захватывает контекст, а не только лямбда. Так как оператор def является просто-напросто присваиванием чуть по-другому отформатированной лямбды:
def a(i): def f(): return i return f i = 1 ff = a(3) i = 2 print (ff())
Простыня на IL для того чтобы вы посмотрели, что контекст захвачен, а Action-ном там не пахнет
Я этого и не собирался отрицать.
zzzzzzerg
20.01.2025 09:45Вы ошибаетесь. Ваш последний пример работает не так как бы вам хотелось:
а объекты и даже типы, соответствующие этим именам, вы можете менять в любой момент, переопределяя имена, и лямбда всё это съест.
Если у вас захвачена переменная из Enclosing scope (как у меня в примере idx_copy) то "честным" способом их значения нельзя поменять в месте вызова лямбды. Я вам предлагал убедиться заглянут в дебагер, я дал вам ссылки на описание co_freevars, а вы продолжаете одно и тоже повторять.
Уфф.
vadimr
20.01.2025 09:45Вы ошибаетесь. Ваш последний пример работает не так как бы вам хотелось
В каком смысле не так как бы мне хотелось? Он возвращает значение 3 из лексического контекста замыкания функции f. Без всяких явных лямбда-выражений.
Вы ж выше написали, что “лямбда-выражение не функция” и “понятно, что в контексте нашего разговора мы говорим не просто о функции, а о функции и захваченном контексте”. Или что вы этим хотели сказать? Чем лямбда-выражение не функция?
Если у вас захвачена переменная из Enclosing scope (как у меня в примере idx_copy) то "честным" способом их значения нельзя поменять в месте вызова лямбды.
Их нельзя просто взять и тупо поменять только по той технической причине, что они могут находиться в чужом лексическом контексте и поэтому недоступны для использования в операторе присваивания (хотя всё равно можно забраться в чужой контекст через другое замыкание). Грубо говоря, любое имя переменной – это на самом деле имя плюс область видимости, и это не имеет никакого прямого отношения к теме лямбд и замыканий.
Но если вы поменяете значение захваченной переменной (например, находясь в одной с ней области видимости и просто присвоив, либо каким-то более сложным образом), то лямбда будет использовать новое значение:
def a(i): def f(): return i def s(n): nonlocal i i = n return f,s i=1 ff,ss = a(555) i=2 print(ff()) ss(777) i=3 print(ff())
Обратите, кстати, внимание, что в этой программе два разных замыкания ff и ss используют один и тот же лексический контекст имени i.
zzzzzzerg
20.01.2025 09:45Я не знаю зачем вы мне показываете мои же примеры, зачем вы вырываете мои слова из контекста и хотите, чтобы я сам себя же опровергал.
Это довольно скучно и мне реально надоело — оставайтесь при своем мнении.
vadimr
20.01.2025 09:45Ну вырвите тоже какие-нибудь мои слова из контекста.
От вас и вашего товарища по несчастью общаться со мной я услышал здесь следующие утверждения относительно семантики языка Python, которые я считаю неверными и оспаривал (поправьте меня, если я неправ относительно фактологии спора):
Что лямбда является объектом в смысле ООП, который сохраняет в своих полях значения свободных переменных (вариант – содержит внутри себя свой лексический контекст).
Что функция отличается от именованной лямбды чем-то, кроме очевидного отличия в синтаксисе.
Что замыкания захватывают значения свободных переменных.
Что эти гипотетические захваченные замыканием значения нельзя изменить извне замыкания.
Всё. Если вы согласны, что эти четыре утверждения в том буквальном виде, как они здесь написаны - чепуха, то причин для спора нет.
zzzzzzerg
20.01.2025 09:45Не согласен.
Не знаю, что вы подразумеваете под "в смысле ООП" - дайте ваше определение, чтобы потом не доказывать вам, что я имел в виду совсем не то, что вы услышали.
Я говорил это в контексте C#, там различия есть. В python отличия в том, что lambda имеет больше ограничений. Но с точки зрения работы замыканий отличий нет (ранее я вам предлагал посмотреть на dis и заметить там вызов MAKE_FUNCTION, что как бы намекает).
Замыкания захватывают объекты, содержащие значения свободных переменных. Я выше признал, что имел в виду это. И также указал, что я против вашего утверждения, что захватываются только имена.
Я не утверждал, что им нельзя изменить значения извне. Я утверждал, что обычным способом эти значения в месте вызова замыкания изменить нельзя, как мы, например, можем сделать с объектом в глобальном скопе - и в этом случае они как раз захвачены только по именам. Свободным переменным можно изменить значения извне потому что python позволяет залезть грязными руками в
__closure__
у функции. Да это документировано, но это работа с внутренним состоянием, что конечно не поощряется. Можете считать это оптимизацией со стороны python; если посмотрите у замыканияco_code
, увидите обращение к свободным переменным по индексам, а не по именам.
Основной предмет спора был в том, что вы утверждали, что захватываются только имена. Также вы утверждали, что захватывается контекст. Я приводил примеры и "доказывал", что для enclosing scope это не так. Я утверждал, что захватываются только те объекты, которые в лямбде используются. При этом если enclosing scope содержит другие переменные, то доступа к ним в точке вызова замыкания мы никак не получим. Потому что у замыкания нет доступа к этому контексту, а только к свободным переменным и/или глобальным (захваченным по именам).
vadimr
20.01.2025 09:45Основной предмет спора был в том, что вы утверждали, что захватываются только имена.
Я действительно это утверждал (подразумевая, конечно, имена в их лексическом контексте), но никогда бы не подумал, что это основной предмет спора.
Я продолжаю это утверждать, иначе как вы можете объяснить этот пример?
def a(i): def f(): return i def s(): nonlocal i i = s return f,s ff,ss = a(555) f=ff() print(f,id(f)) ss() f=ff() print(f,id(f))
Какой объект в данном случае захватывается в качестве i замыканием f(), если захватывается объект, а не имя? Когда образовалось замыкание, имя i имело значение числа 555, а потом то же самое имя i получило значение функции s. Это явно два разных объекта.
Именно имя является элементом лексического контекста.
Также вы утверждали, что захватывается контекст. Я утверждал, что захватываются только те объекты, которые в лямбде используются.
Я не очень понял это утверждение. Вы можете привести в питоне пример какого-нибудь объекта, который не используется в функции/лямбде, но присутствует в её лексическом контексте?
Насколько я представляю себе питон, объект из enclosing scope появляется в области видимости (т.е. лексическом контексте) тела функции только по факту упоминания в тексте этой функции своего значения или использования в операторе nonlocal или global. Если имя из enclosing scope явно не упомянуто в функции, то оно не затягивается в её лексический контекст, и мы конечно не можем его использовать. Но это не имеет отношения к замыканиям, это просто правило видимости имён.
Поэтому, на мой взгляд, наши формулировки здесь эквивалентны. По крайней мере, я не могу построить различие.
Теперь по пунктам.
Не знаю, что вы подразумеваете под "в смысле ООП" - дайте ваше определение, чтобы потом не доказывать вам, что я имел в виду совсем не то, что вы услышали.
Я имею в виду, что есть какой-то конкретный питоновский класс, экземпляром которого является лямбда, и этот экземпляр сохраняет внутри себя, в своей памяти, значения свободных переменных или лексический контекст, а не просто ссылается на контекст.
Свободным переменным можно изменить значения извне потому что python позволяет залезть грязными руками в
__closure__
у функции. Да это документировано, но это работа с внутренним состоянием, что конечно не поощряется.Я вам выше привёл совершенно корректный и теоретически правильный способ изменения свободной переменной извне захватившего её замыкания. Он не является хаком и не использует руками поле __closure__. Это штатное использование механизма замыканий. Поощрять такой стиль программирования или не поощрять – другой вопрос, но это штатный механизм языка.
Кроме того, элементы лексического контекста замыкания не обязательно ведь должны быть неактивны после формирования замыкания. Мы можем отдать замыкание из нашего контекста в другую лексическую область, а сами продолжать работать в своём лексическом контексте в параллельной нитке, например. И продолжать изменять нашу переменную внутри её лексического контекста, но при этом извне содержащего её замыкания.
Про C# не буду с вами спорить.
zzzzzzerg
20.01.2025 09:45Я немного расширил ваш пример:
def a(i): def f(): return i def s(): nonlocal i i = s return f,s ff,ss = a(555) f=ff() print(f,id(f)) print(ff.__closure__, ff.__code__.co_freevars, (id(ff.__closure__[0]), ff.__closure__[0].cell_contents)) print(ss.__closure__, ss.__code__.co_freevars, (id(ss.__closure__[0]), ss.__closure__[0].cell_contents), (id(ss.__closure__[1]), ss.__closure__[1].cell_contents)) ss() f=ff() print(f,id(f)) print(ff.__closure__, ff.__code__.co_freevars, (id(ff.__closure__[0]), ff.__closure__[0].cell_contents)) print(ss.__closure__, ss.__code__.co_freevars, (id(ss.__closure__[0]), ss.__closure__[0].cell_contents), (id(ss.__closure__[1]), ss.__closure__[1].cell_contents)) print ('end')
Вот его вывод:
555 2178338577904 (<cell at 0x000001FB2F172080: int object at 0x000001FB2F18DDF0>,) ('i',) (2178338463872, 555) (<cell at 0x000001FB2F172080: int object at 0x000001FB2F18DDF0>, <cell at 0x000001FB2F172140: function object at 0x000001FB2F18BB00>) ('i', 's') (2178338463872, 555) (2178338464064, <function a.<locals>.s at 0x000001FB2F18BB00>) <function a.<locals>.s at 0x000001FB2F18BB00> 2178338568960 (<cell at 0x000001FB2F172080: function object at 0x000001FB2F18BB00>,) ('i',) (2178338463872, <function a.<locals>.s at 0x000001FB2F18BB00>) (<cell at 0x000001FB2F172080: function object at 0x000001FB2F18BB00>, <cell at 0x000001FB2F172140: function object at 0x000001FB2F18BB00>) ('i', 's') (2178338463872, <function a.<locals>.s at 0x000001FB2F18BB00>) (2178338464064, <function a.<locals>.s at 0x000001FB2F18BB00>)
Теперь попробую ответить на ваш вопрос:
Какой объект в данном случае захватывается в качестве i замыканием f(), если захватывается объект, а не имя? Когда образовалось замыкание, имя i имело значение числа 555, а потом то же самое имя i получило значение функции s. Это явно два разных объекта.
Как вы можете видеть в выводе замыкание ff содержит только одно захваченное значение/объект. Значение хранится в специальном прокси-объекте - Cell Objects. И таким образом мы храним в замыкании прокси-объект со значением.
Замыкание ss содержит уже две захваченных свободных переменных - i и s. И при выполнении замыкания ss значение в прокси-объекте (в котором хранится значение i) заменяется значением s. Это видно по тому, что id прокси-объектов не изменились, а значения поменялись.
Это также хорошо видно в ассемблере. Вот для ff:
0 COPY_FREE_VARS 1 2 RESUME 0 4 LOAD_DEREF 0 6 RETURN_VALUE
Вот для ss:
0 COPY_FREE_VARS 2 2 RESUME 0 4 LOAD_DEREF 1 6 STORE_DEREF 0 8 LOAD_CONST 0 10 RETURN_VALUE
Так что "Это явно два разных объекта." не является верным утверждением.
Я не очень понял это утверждение. Вы можете привести в питоне пример какого-нибудь объекта, который не используется в функции/лямбде, но присутствует в её лексическом контексте?
Насколько я представляю себе питон, объект из enclosing scope появляется в области видимости (т.е. лексическом контексте) тела функции только по факту упоминания в тексте этой функции своего значения или использования в операторе nonlocal или global. Если имя из enclosing scope явно не упомянуто в функции, то оно не затягивается в её лексический контекст, и мы конечно не можем его использовать. Но это не имеет отношения к замыканиям, это просто правило видимости имён.
Если мы понимаем лексический контекст лямбы так узко, то я с этим согласен.
Я вам выше привёл совершенно корректный и теоретически правильный способ изменения свободной переменной извне захватившего её замыкания.
Я не согласен. Это "жульничество". У обоих замыканий захватывается один и тот же "объект". Я говорил про случаи, когда вы находитесь за пределами области видимости, в которой объявлено замыкание. А так вы продемонстрировали функциональную реализацию инкапсуляции.
Кроме того, элементы лексического контекста замыкания не обязательно ведь должны быть неактивны после формирования замыкания. Мы можем отдать замыкание из нашего контекста в другую лексическую область, а сами продолжать работать в своём лексическом контексте в параллельной нитке, например. И продолжать изменять нашу переменную внутри её лексического контекста, но при этом извне содержащего её замыкания.
Я вам уже приводил пример:
def create(): lambdas = [] idx_copy = 0 for idx in range(10): idx_copy = idx + 1 print ('-', id(idx), id(idx_copy)) lambdas.append(lambda : print(idx_copy, idx)) del idx_copy return lambdas
Если мы продолжим работать с idx_copy после формирования замыкания (внутри функции create - надеюсь что вы это имели в виду), то мы либо будет менять значение, которое отразиться на замыкании, либо мы вообще можем сломать замыкание, если используемую переменную удалим.
Мы можем отдать замыкание из нашего контекста в другую лексическую область, а сами продолжать работать в своём лексическом контексте в параллельной нитке, например.
Вот это мне не понятно, приведите пример, чтобы у нас не было разногласий.
Я имею в виду, что есть какой-то конкретный питоновский класс, экземпляром которого является лямбда, и этот экземпляр сохраняет внутри себя, в своей памяти, значения свободных переменных или лексический контекст, а не просто ссылается на контекст.
Конечно есть "питоновский" класс - Function Objects, который хранит ссылку на замыкание - cpython/funcobject.h. Замыкание является кортежом/туплом ячеек Cell Objects.
vadimr
20.01.2025 09:45Как вы можете видеть в выводе замыкание ff содержит только одно захваченное значение/объект. Значение хранится в специальном прокси-объекте - Cell Objects. И таким образом мы храним в замыкании прокси-объект со значением.
Если под объектом вы понимаете этот прокси-объект, то я снимаю свои возражения. Как видите, он хранит как раз имена и служебную информацию о контексте.
Я не согласен. Это "жульничество". У обоих замыканий захватывается один и тот же "объект". Я говорил про случаи, когда вы находитесь за пределами области видимости, в которой объявлено замыкание. А так вы продемонстрировали функциональную реализацию инкапсуляции.
Я не согласен, что это жульничество, потому что это вполне нормальная функциональная техника. Но это не так важно, потому что давайте сразу обсудим затребованный вами пример с ниткой:
import threading, time def a(i): class T(threading.Thread): def run(self): nonlocal i for j in range(100000000): i = i+1 T().start() def f(): return i return f f = a(0) print(f()) time.sleep(1) print(f())
Является ли он жульническим?
Здесь класс T находится за пределами замыкания f(), но использует общий с ним лексический контекст для переменной i и поэтому легко её меняет. А применение замыкания вообще происходит в другом месте.
Конечно есть "питоновский" класс - Function Objects, который хранит ссылку на замыкание - cpython/funcobject.h. Замыкание является кортежом/туплом ячеек Cell Objects.
Так этот класс через ячейки ссылается на самостоятельные объекты, представляющие имена и связанные с ними значения, а не содержит значения в себе самом. К примеру, если уничтожить замыкание, то участвующие в нём переменные останутся целы, если на них есть другие ссылки. Я говорил об этом.
zzzzzzerg
20.01.2025 09:45Как видите, он хранит как раз имена и служебную информацию о контексте.
Скажите, пожалуйста, откуда вы так решили?
Здесь класс T находится за пределами замыкания f(), но использует общий с ним лексический контекст для переменной i и поэтому легко её меняет. А применение замыкания вообще происходит в другом месте.
Понятно, это аналогично тому, что мы уже разобрали. Тут ничего интересного нет.
Так этот класс через ячейки ссылается на самостоятельные объекты, представляющие имена и связанные с ними значения, а не содержит значения в себе самом.
Опять же, скажите, пожалуйста, откуда вы это взяли?
К примеру, если уничтожить замыкание, то участвующие в нём переменные останутся целы, если на них есть другие ссылки. Я говорил об этом.
Это вообще не относится к предмету разговора.
vadimr
20.01.2025 09:45Как видите, он хранит как раз имена и служебную информацию о контексте.
Скажите, пожалуйста, откуда вы так решили?
Так в вашей собственной распечатке двумя сообщениями выше это написано. Прямо имена распечатаны.
Понятно, это аналогично тому, что мы уже разобрали. Тут ничего интересного нет.
Неважно, интересно это или неинтересно, но это является контрпримером к вашему утверждению, что свободным переменным якобы нельзя изменить значения извне замыкания, не используя трюков с __closure__. Это самые обычные переменные, которые не находятся ни под каким арестом. Просто замыкание обладает доступом к их лексическому контексту, которым применяющий его код может напрямую не обладать. Но это чисто технический вопрос определения области видимости имён в каждой точке программы, который не является принципиальным запретом и не имеет прямого отношения к самому замыканию и его применению.
Так этот класс через ячейки ссылается на самостоятельные объекты, представляющие имена и связанные с ними значения, а не содержит значения в себе самом.
Опять же, скажите, пожалуйста, откуда вы это взяли?
Прочите, пожалуйста, внимательнее. Вроде бы я написал очевидную вещь – что переменные существуют независимо от использующего их замыкания. В с этим хотите спорить?
К примеру, если уничтожить замыкание, то участвующие в нём переменные останутся целы, если на них есть другие ссылки. Я говорил об этом.
Это вообще не относится к предмету разговора.
Это как раз напрямую относится к предмету разговора, потому что является одним из следствий того, что эти переменные не являются собственными частями объекта, представляющего замыкание.
zzzzzzerg
20.01.2025 09:45Так в вашей собственной распечатке двумя сообщениями выше это написано. Прямо имена распечатаны.
Вы невнимательно прочитали мой код, и не читали ссылки на Cell Object.
Неважно, интересно это или неинтересно, но это является контрпримером к вашему утверждению, что свободным переменным якобы нельзя изменить значения извне замыкания, не используя трюков с __closure__.
Еще раз - извне общего контекста это сделать нельзя. Ваш пример с потоком эквивалентен вашему примеру с ff и ss.
Так этот класс через ячейки ссылается на самостоятельные объекты, представляющие имена и связанные с ними значения, а не содержит значения в себе самом.
Опять же, скажите, пожалуйста, откуда вы это взяли?
Прочите, пожалуйста, внимательнее. Вроде бы я написал очевидную вещь – что переменные существуют независимо от использующего их замыкания. В с этим хотите спорить?
Вы в двух своих высказываниях написали разное. Я вам задал конкретный вопрос - откуда вы вот это взяли "Так этот класс через ячейки ссылается на самостоятельные объекты, представляющие имена и связанные с ними значения, а не содержит значения в себе самом.". Но как выяснили уже выше - вы ни мой код не можете нормально прочитать, ни ссылки не читаете.
Это как раз напрямую относится к предмету разговора, потому что является одним из следствий того, что эти переменные не являются собственными частями объекта, представляющего замыкание.
Ну разберитесь уже.
vadimr
20.01.2025 09:45Еще раз - извне общего контекста это сделать нельзя. Ваш пример с потоком эквивалентен вашему примеру с ff и ss.
Извне общего контекста, разумеется, этого сделать нельзя. Но вы-то писали, что этого нельзя сделать извне замыкания.
Я вам задал конкретный вопрос - откуда вы вот это взяли "Так этот класс через ячейки ссылается на самостоятельные объекты, представляющие имена и связанные с ними значения, а не содержит значения в себе самом."
Вот это вот – что такое?
(<cell at 0x000001FB2F172080: int object at 0x000001FB2F18DDF0>, <cell at 0x000001FB2F172140: function object at 0x000001FB2F18BB00>) ('i', 's') (2178338463872, 555) (2178338464064, <function a.<locals>.s at 0x000001FB2F18BB00>)
Не ссылка на имена i и s и их значения в их контексте?
Но как выяснили уже выше - вы ни мой код не можете нормально прочитать, ни ссылки не читаете.
Вы опять не выдерживаете предметную дискуссию по существу и начинаете хамить. Это некрасиво, даже если вы в чём-то были неправы.
Я говорю по существу очень простую вещь: что захвата переменных в лямбде по значению, как в C++, в питоне (и вообще в ФП) нет. А вы зачем-то с этим спорите.
zzzzzzerg
20.01.2025 09:45Извне общего контекста, разумеется, этого сделать нельзя. Но вы-то писали, что этого нельзя сделать извне замыкания.
Я писал следующее:
Если у вас захвачена переменная из Enclosing scope (как у меня в примере idx_copy) то "честным" способом их значения нельзя поменять в месте вызова лямбды.
(отредактировано)
~Это вы не выдерживаете предметную дискуссию.~ Впрочем могу признать, что вариант с двумя лямбдами решает эту проблему.
(отредактировано)
Вот это вот – что такое?
(<cell at 0x000001FB2F172080: int object at 0x000001FB2F18DDF0>, <cell at 0x000001FB2F172140: function object at 0x000001FB2F18BB00>) ('i', 's') (2178338463872, 555) (2178338464064, <function a.<locals>.s at 0x000001FB2F18BB00>)
Не ссылка на имена i и s и их значения в их контексте?
Если бы внимательно посмотрели код, то увидели бы, что имена печатаются не из замыкания, а из code object, и я вам показывал в ассемблере, что эти имена не используются при доступе к захваченным переменным. Посмотрите внимательнее.
Вы опять не выдерживаете предметную дискуссию по существу и начинаете хамить. Это некрасиво, даже если вы в чём-то были неправы.
Некрасиво невнимательно работать с материалом, который дают вам коллеги. И это не хамство, а сожаление о том, что спор с вами бесполезен.
Я говорю по существу очень простую вещь: что захвата переменных в лямбде по значению, как в C++, в питоне (и вообще в ФП) нет. А вы зачем-то с этим спорите.
Ну вот вы и опять передергиваете. Вы во-первых, это нигде не утверждали ранее, во-вторых, в самом начале дискуссии я написал следующее - "В С++ можно выбрать разные режимы захвата, в Python приходится выкручиваться.", в-третьих вы утверждали, что захватываются только имена, и я спорил с этим и аргументировано свою точку зрения обосновал. Тут вы опять не выдерживаете предмет дискуссии.
vadimr
20.01.2025 09:45Я вам напомню, что вы писали:
4. Что эти гипотетические захваченные замыканием значения нельзя изменить извне замыкания.
Я не утверждал, что им нельзя изменить значения извне. Я утверждал, что обычным способом эти значения в месте вызова замыкания изменить нельзя, как мы, например, можем сделать с объектом в глобальном скопе - и в этом случае они как раз захвачены только по именам. Свободным переменным можно изменить значения извне потому что python позволяет залезть грязными руками в
__closure__
у функции.На мой взгляд, здесь написано, что значение захваченной замыканием переменной можно изменить извне замыкания по причине возможности грязного хака с __closure__.
Я вам показал, что это не так, и, более того, пытаюсь донести мысль, что возможность изменения захваченных замыканием переменных не имеет вообще никакого отношения к самому замыканию. Замыкание – это просто способ сохранения подразумевавшегося программистом лексического контекста для тела функции, ничего больше.
Если бы внимательно посмотрели код, то увидели бы, что имена печатаются не из замыкания, а из code object
Так замыкание – это в реализации и есть code object плюс вспомогательная информация.
Давайте уж тогда определимся, что я пишу о (денотационной) семантике языка, и под “замыканием” имею в виду семантическую конструкцию языка Python, а не какую-то внутреннюю структуру действующего интерпретатора.
и я вам показывал в ассемблере, что эти имена не используются при доступе к захваченным переменным.
Правильно, потому что в целях оптимизации там же наряду с этими именами хранятся закешированные ссылки на их значения. Но как только вы поменяете значение у имени, так сразу этот кеш перестроится и лямбда будет использовать новое значение, и я вам это показывал в работающем коде. Что же тогда означают ваши слова, что значения “захватываются”? В каком смысле они захватываются, если любой может их поменять?
Ну вот вы и опять передергиваете. Вы во-первых, это нигде не утверждали ранее, во-вторых, в самом начале дискуссии я написал следующее - "В С++ можно выбрать разные режимы захвата, в Python приходится выкручиваться.", в-третьих вы утверждали, что захватываются только имена, и я спорил с этим и аргументировано свою точку зрения обосновал. Тут вы опять не выдерживаете предмет дискуссии.
Я рад, если часть противоречий удалось снять. Тем не менее, я продолжаю настаивать, что с точки зрения семантики языка захватываются только имена, а если интерпретатор для эффективности содержит рядом с ними в своих структурах копии ссылок на из значения, то это не значит, что захватываются сами значения. Что я, в свою очередь, аргументированно обосновал.
zzzzzzerg
20.01.2025 09:45Тем не менее, я продолжаю настаивать, что с точки зрения семантики языка захватываются только имена
Продолжайте.
3263927
20.01.2025 09:45помню когда писал свою первую программу (управление производством) на C# после похожего проекта на C++... было ощущение что ходил всю жизнь на костылях а потом ноги выросли
сейчас C/C++ использую только для программирования микроконтроллеров и каких-то узких задач, большие программные комплексы никогда бы не стал на нём делать
orenty7
20.01.2025 09:45Лямбды, по-хорошему, должны быть настолько же мощными, насколько обычные функции. В питоновских лямбдах можно писать выражения, но нельзя, в отличие от функций, циклы
dmitry_rozhkov
20.01.2025 09:45А есть хорошая C++ библиотека, реализующая Communicating Sequential Processes типа горутин с каналами?
Kelbon
20.01.2025 09:45в С++ есть выбор, стеклес или стекфул корутины,
стеклес:
https://github.com/kelbon/kelcoro
стекфул:
userver (фреймворк) или boost context и тд
AdrianoVisoccini
20.01.2025 09:45Когда в последний раз вы получали настоящее удовольствие от программирования?
Честно? Вот как после того как забросил программирование именно из-за С++ который вверг меня в настоящую депрессию и после огромного перерыва в 10 лет сел писать на Java, пя получаю удовльствие КАЖДУЮ МИНУТУ КАЖДУЮ КАРЛ
ОНО САМО ВСЕ ПИШЕТ, ВСЕ РАБОТАЕТ, ПАМЯТЬ ЧИСТИТ САМО, ЛОМБОК ВСЕ САМ ГЕНЕРИРУЕТ ЗА ТЕБЯ
я сажусь писать код с улыбкой, пишу и хохочу вслух как раз как клоун Роксо из вашего поста. Боже, писать не на С++ это такое наслаждение, не сравнимое не с чем в мире. Как говорится - дай человеку козу, чтобы он понял как хорошо жилось без козы, так же и у меня с С++JKot
20.01.2025 09:45А я как автор, начинал с плюсов, ушёл в java/go/c# лет на 10 и по итогу вернулся к плюсам и неистово кайфую. В общем каждому своё.
segment
20.01.2025 09:45А какой в итоге код получается в production на плюсах? Сколько ни смотрел на большие проекты, видел в основном более процедурный стиль (что-то близкое к Си с классами), ну там немного обмазано умными указателями и несколько стандартных контейнеров. На C# как раз можно писать используя его сахар и все остается понятным, но на плюсах как получается?
Jijiki
когда очередная фишка или функционал что запланировал - доделано или работает, а работает значит нифига не заметно/ничего не поменялось, но было желание и изучение - это праздник. когда матеша заработала, реально обрадовался, я её долго смотрел тестил, пробовал/понимал
второй раз прифигел когда обнаружилось что нормали можно скейлить (кучность), и почти за дешево знать коллизию с террейном, после расчета (там с формулами еще есть) - радости тоже не было предела
математика в 3д интересна, почти весь мат аппарат буквально в базе лежит, чтобы отрисовать нужны векторы, матрицы, кватернионы. тоесть можно пойти от визуала не думать о математике, но потом можно столкнуться что там всё это число
про шаблоны тоже заметил, нужен баланс шаблонов/не шаблонов