Комментарии 43
Часто под подобными статьями про плюсы вылезают люди с коментариями типа "вот по этому плюсы плохи! Что за UB(например) в 21 веке".
Всегда считал такие коментарии клоунадой. Мол, не знаешь - не пользуйся, зачем пришел сюда коментировать.
К чему я это? А к тому, что разное поведение def foo(bar=1) и def foo(bar); bar=1 это плохо. Так нельзя в 21 веке.
А к тому, что разное поведение def foo(bar=1) и def foo(bar); bar=1 это плохо.
А разное повдение y=x; x=x+1 и x=x+1; y=x? Это разные вещи и делают они разное. Более того, оно иногда и используется намеренно (хотя мы осуждаем).
А ничего, что во втором случае передача параметра вообще не имеет смысла, так как его значение не используется? Какой-то у вас пример слишком синтетический, чтобы можно было понять посыл.
А оно с bar=1 одинаковое, вот с bar=[] будет разное.
Вообще согласен, непонятно, зачем в Python 3 не сделали интерпретацию аргументов по умолчанию как выражений и не сделали нормальные области видимости, коль уж всё равно с Python 2 обратную совместимость сломали.
Это и есть выражения, просто они вычисляются в момент создания функции. Иначе бы возникли другие вопросы и другие претензии.
А какие бы возникли вопросы?
Вот в Julia значения по умолчанию вычисляются в момент вызова функции, и там это ни у кого проблем не вызывает. Бонусом это даёт возможность ссылаться в значении по умолчанию на предыдущие аргументы. Наоборот, предостережения видел только в руководствах по Python и по Common Lisp. Причём в Lisp это относится только к литералам типа '(1 2), т.к. они по стандарту могут быть вычислены однократно при определении функции, вместо этого рекомендуется значения по умолчанию просто задавать как (list 1 2), тогда они гарантированно вычисляются в момент вызова.
я не знаю julia, можете показать как будут выгдятеть аналоги этого кода?
def new_generator(default: int):
def number_generator(arg: int=default):
return arg
return number_generator
def baz(generator=new_generator(1)):
passПримерно так же, как и в Python
function make_generator(default::Integer)
function generator(arg::Integer=default)
return arg
end
return generator
end
function baz(generator=make_generator(1))
return nothing
end
Но это, в общем, неинтересно. Интереснее то, что с мутабельными аргументами будет всё то же самое, без танцев с None.
function make_collector(init::Vector=[])
function collector(args...)
return append!(init, args...)
end
return collector
end
julia> c1 = make_collector();
julia> c2 = make_collector();
julia> c1(3, 4, 5)
3-element Vector{Any}:
3
4
5
julia> c2(5, 12, 13)
3-element Vector{Any}:
5
12
13
julia> c1(5, 12, 13)
6-element Vector{Any}:
3
4
5
5
12
13
Подозреваю, что дело в банальной производительности. Как я понял Julia - язык компилируемый и там это не так критично. А вот в скриптовом могут быть проблемы. Замедлять код на 50-10-20-50% из фичи, которая используется в 3% случаев, если не реже, так себе решение.
Для меня подобное поведение не новость, потому что я сам язык хорошо знаю. А одно из первых правил питона гласит: будь аккуратен с мутабельными объектами.
Для Python 1/2 я ещё мог бы согласиться с предположением о производительности. Для Python 3 - сомнительно. Учитывая, что этот пример с мутабельным аргументом по умолчанию упоминается в каждом первом руководстве для новичков как типичный ногострел - ну уж можно было бы подумать, как его убрать. Опять же, с учётом поломки обратной совместимости в любом случае. Думаю, можно это было сделать без потерь производительности, Лисп же как-то справляется, в конце концов. Скорее, никто из core developer'ов не счёл такое поведение проблемным.
С нормальными областями видимости, такое ощущение, та же история - не было в core developer'ах ярого функциональщика, которого бы поведение из 4-го примера прямо бесило. А так плоская область видимости внутри функции особо не мешает, пока не начинаешь активно пользоваться замыканиями.
Думаю, если питон основной язык - то к особенностям привыкаешь и перестаёшь замечать, но вот если надо постоянно переключаться с одной семантики на другую - это раздражает.
Ага, если тысячи юзеров постоянно жмут не ту кнопку - надо менять кнопку, а не писать сотни мануалов о том, какую кнопку жать. Тем более что смысла делать именно такое поведение особо не видно. Это какая-то обрезанная разновидность статических переменных, но только для контейнеров? Могли бы просто запретить ее на уровне языка и все.
Это не обычные юзеры, а программисты, которые себя считают умными и должны бы в состоянии документацию прочитать. Ну и на счёт сотни мануалов Вы преувеличиваете.
Я считаю, что в любом ЯП есть тёмные области, поведение в которых выглядит странным. Про такие области надо знать и не ходить туда. Я думаю, что в принципе невозможно создать полноценный ЯП без "забавных особенностей".
Вы предлагаете проверять мутабельность объекта? Здорово. Как кастомные объекты проверять будем? Обозначать их мутабельность руками? А если он обозначен как немутабельный, а на самом деле что-то изменяет? Вот на ровном месте проблемы создать можно.
У программистов и так полно чего нужно держать в контексте. Забивать при этом голову еще и картой минных полей конкретного ЯП - излишнее усложнение. Насчет сотни не преувеличиваю. Только за последние месяцы этот злополучный param: list=[] встретился в разных каналах с десяток раз
Но стремиться к их сокращению - стоит. В конце концов, бездушную машину приятнее нагружать, чем себя. К счастью, есть линтеры, которые ловят то, что не хочет ловить сам интерпретатор
Есть же понятие хешируемости для применения в качестве ключей. Либо можно в принципе запретить любое значение дефолта, кроме элементарных базовых.
Ну так то каналы. А в реальном коде? Там, где я встречал, можно было и без этого обойтись.
Вот Вы сами решение и нашли. По мне так, это издержки того, что пишешь на многих языках. Ты знаешь либо один хорошо, либо много так себе. Электронные помощники спешат на помощь!
Написать хеш метод - минутное дело. И это вообще ничего не говорит об мутабельности объекта. А на счёт базовых, в том то и дело, что они зашиты внутри, переменные выглядят для него одинаковыми. Да и если запретить, то мне что, нельзя использовать мною придуманый, особо точный Decimal(5/2)?
Там, где я встречал, можно было и без этого обойтись
Написать хеш метод - минутное дело. И это вообще ничего не говорит об мутабельности объекта
Всегда можно без этого, да, но речь не об этом. В моей картине мира подобные вещи - как открытый канализационный люк. Он должен быть всячески огорожен знаками, предупреждениями и заборами, чтобы исключить ненамеренное попадание (в контексте ЯП - не давать попасть туда, используя простые и типичные конструкции). Но если ты идешь туда целенаправленно, игнорируя знаки - предполагается, что ты знаешь, что делаешь, и готов к последствиям (в контексте ЯП - если ты задаешь дефолтное значение мутабельного объекта с переопределенным хешем, это явно не типичная штука, и тут уже сам несешь ответственность).
О, мы пришли к разному пониманию ЯП. По мне, так это инструмент: есть крутой, с отдельной камерой, автоматами аварийной защиты, двойной изоляцией и прочее. А есть простой, как скальпель, где пораниться самому проще, чем сделать что-то полезное. Ну и кучу всего между ними. И перед тем, как брать в руки инструмент, надо пройти ТБ: знать, на что он способен и как работать так, чтобы без рук не остаться.
Думаю, если питон основной язык - то к особенностям привыкаешь и перестаёшь замечать,
Вот тут полностью соглашусь.
Касаемо примера. Может я уже адаптировался к написанию на Питоне, но как по мне, этот пример несколько синтетический. Я не очень представляю себе ситуацию, когда нам было бы без разницы, как обрабатывать данные, со списком или нет. Ну т.е. либо ты это хочешь вставить в конкретный контейнер, либо он тебе совсем не нужен. Единственное, что могу припомнить, так это рекурсивные функции. Но даже там лучше убрать дефолтное значение и передавать пустой список при граничных условиях. А в приведённом примере функция выполняет два действия: обработку значения и вставку его в контейнер. А такое надо разбивать на два. Если же нам нужны данные из контейнера (по типу последовательности Фибоначчи), так тут лучше свёртку использовать (reduce).
В общем, если подытожить, то тут должна быть скорее лекция о том, почему в принципе списки не должны выступать дефолтными значениями.
Касаемо самого поведения. Могу предложить ещё одну причину. Предложенное Вами изменение нарушит простое правило Питона:
Данные передаются по ссылке. Всегда.
И тогда у нас может возникнуть вопрос, а почему тогда:
x = [1,2,3,4,5]
y=x
y.append(6)
print(x)Оба этих явления одного порядка.
Скрытый текст
Равно как и поведение в третьем примере.
А это уже совсем другая ситуация.
И ведь что характерно, изменения на этом могут не остановиться, а начать менять всё это и в других местах. Тогда мы вместо простого и лаконичного правила получим кучу условий здесь делаем так, тут этак, а тут сяк. Я за консистентное решение.
>Я не очень представляю себе ситуацию, когда нам было бы без разницы, как обрабатывать данные, со списком или нет
Например, если внутри там for item in lst: ... Нет списка в аргументах - подставляется пустой - ничего не делается, без каких-либо доп условий. А так пришлось бы писать lst or [] или if lst: for item in lst или еще что-то, а если таких мест много, то lst = lst or [].
А не заменяется ли это на
if not list:
return []Если нечего обрабатывать, то там и думать нечего как правило.
Опять же, что может заставить вас использовать такую функцию без аргументов? Оно ж тогда вообще теряет смысл. Ну т.е. если мы обрабатываем список, да ещё и в нескольких местах, то наверное мы его очень хотим получить.
Вы придираетесь к упрощенным примерам, которые по определению могут не иметь смысла. Ну ок, как насчет опционального списка-фильтра-стоп-листа для некой выборки. Он вполне мб пустым. И предположим, что он пробрасывается еще в десяток мест внутри, в т.ч. в какой-нибудь си через не очень тщательно продуманное АПИ, которое может не ожидать null вместо массива.
Я придираюсь потому, что в реальности не помню таких случаев. Хорошо, пусть будет чёрный список. Не тривиальна ли тогда обработка случая, когда его нет или он пустой? А если он может потом дополниться, то не лучше ли сначала его полностью собрать, а уже потом с ним что-то делать. Я к тому, что подобная запись свидетельствует о проблемах с кодом.
И я опять прихожу к тому, что это может быть какое-нибудь легаси, которое трогать страшно. Но легаси на то и легаси, что мы смотрим на него и умиляемся.
Да тривиальна конечно, но она требует доп строчек, тогда как пустой список - нет.
К тому же не могу представить, зачем вообще может понадобиться конструкция lst: list=[] в текущем виде. Т.е. имеем не просто открытый люк, а люк, в который никто не лазает, только случайно падает. Ну и на фиг он нужен?
Я не очень представляю себе ситуацию, когда нам было бы без разницы, как обрабатывать данные, со списком или нет. Ну т.е. либо ты это хочешь вставить в конкретный контейнер, либо он тебе совсем не нужен.
В численных задачах часто опционально передается буфер для промежуточных вычислений или выходных данных. Тут, конечно, несколько другая проблема возникает - что размер буфера будет зависеть от входных данных - но при вычислении выражения для дефолтного значения при вызове функции это можно реализовать, а в текущем подходе уж точно никак. В Лиспе, опять же, дефолтное значение может ссылаться на предыдущие аргументы, т.е. на момент создания питона такое поведение не было совсем уж новаторством.
Данные передаются по ссылке. Всегда.
Ну тут можно и дальше зайти. Почему запись мутабельного объекта в аргументах функции будет передачей данных, а запись в теле функции нет тогда? Для меня, наоборот, более консистентной моделью было бы, что в записи функции всё передаётся by-expression, включая значения аргументов, а вот при вызове эти выражения вычисляются и создают необходимые объекты.
В численных задачах часто опционально передается буфер для промежуточных вычислений или выходных данных.
Благодарю. Тут - да, без этого никак.
в записи функции всё передаётся by-expression, включая значения аргументов, а вот при вызове эти выражения вычисляются и создают необходимые объекты.
Вы говорите про другой ЯП. Как верно было отмечено в статье, def - это команда. С весьма простым поведением.
Ещё замечание: except Exception: таки считается более культурным вариантом, чем просто except:, потому что системные исключения наследуются не от Exception, и программу как раз-таки можно завершить без kill -9.
Шо? Опять? Стабильно раз в месяц появляется подобная статья
# Создаем список функций: lambda x: x0, lambda x: x1 ...multipliers = [lambda x: xi for i in range(5)]# Проверяем. Ожидаем: 20 = 0, 2 * 1 = 2 ...results = [m(2) for m in multipliers]
# Передаем i как аргумент по умолчанию
multipliers = [lambda x, i=i: x * i for i in range(5)]
За такой код я бы канделябром, чесслово.
Если вам нужно последовательно применить изменения к элементам списка, надо использовать map:
>>> list(map(lambda x: x*2, range(5)))
[0, 2, 4, 6, 8]Из смешного, генерация функций тоже работает:
>>> a = list(map(lambda i:(lambda x: x*i), range(5)))
>>> [m(2) for m in a]
[0, 2, 4, 6, 8]На крайняк, если нужно сначала создать именно список функций, сделать сначала декоратор:
>>> def create_handler(i):
...
... def wrapper(x):
... return x*i
... return wrapper
...
>>> a = [create_handler(i) for i in range(5)]
>>> [m(2) for m in a]
[0, 2, 4, 6, 8]Чуть более многословно, зато более читаемо. У нас вроде нет лимитов на использование строчек.
А то научатся гланды чесать через задницу, а потом такие: "Надо быть аккуратным, чтобы руки в говне не измазать."
Ну или хотя бы разделить аргумент и счетчик цикла
multipliers = [lambda x, loc_i=i: x * loc_i for i in range(5)]
А вообще зря создатели в лямбдах не стали отделять параметры скобками, теряется читаемость
Я может неясно выразился, но мой поинт был в другом: не надо писать сложный код, когда есть код простой. Нафига мне помнить о том, как работает замыкание, если этого можно избежать. Проблема предложенного автором "правильного" кода ровно в том же, в чём и код изначальный: его действие неочевидно. Увы, но и Ваш код страдает тем же.
Правильный вариант: писать через декоратор и не мучать людям мозг. В том числе и себе в будущем.
зачем блистать умом, показывая свою "гениальность". пример тебе приведен для наглядности, чтобы ты не вникал в него несколько минут, отходя при этом от темы. все прекрасно понимают (я надеюсь) что писать такой код - это либо безумие, либо утонченный садизм.
Гениальность тут ни при чём. Просто откровенно говёный код предлагают заменить на чуть менее говёный код. Статья рассчитана на новичков, а значит они будут думать, что так - это нормально. Потом переучивай таких.
при чем здесь "говеный" или нет - 1. 2 - работа с асинхронностью для новичков? пупупу. откровенно тупые аргументы ;) 3 - спс что открыл для меня map-у, даже не знал, что так можно. 4 - бросать какахи в чей-то код и говорить: "смотри, вот так надо" и писать ... list(map(lambda ... какие курсы проходил?
Какой ты сложный.
Где ты работу с асинхронностью увидел? 3-й случай? Так это база, в общем. Сейчас в каком-нибудь фреймворке легко её увидеть. Используешь асинхрощину - используй везде. Типично новичковая ошибка.
Читай, что написано: Если нужно, то используй map. Опять же, если плохо написал, вот более развернутая мысль: вместо того, чтобы генерировать функции обработки, лучше применить единообразную обработку списка. Если такой возможности нет, то генерировать надо в явном виде через декораторами. Короче, если есть вариант обойтись без функций - обходимся без них. Если не вариант, то пишем в явном виде.
Давай разбираться.
map(lambda- типовая конструкция вообще-то.listмне нужен исключительно, чтобы в консоль данные вывести, ибо в оригинале там генератор. В цикле я бы его не использовал.Какахами я кидаюсь потому, что понимаю, что здесь идёт переход на тёмную сторону, так сказать. Где-то в книгах при обучении замыканиям, я видел такой пример. Но сейчас понимаю, что ТАК писать нельзя. Именно для того, чтобы не вспоминать, как там замыкания работают. Ибо ошибёшься гарантированно. Как выше отметили: типичный ногострел.
Курсы проходил давно и книг прочёл тоже достаточно. Там и поваренная книга была, и ещё что-то.
в статье неточность:
Переменная
iв глобальной области осталась висеть со значением4. И все 5 ваших лямбд радостно лезут ...
i не сохраняется в глобальном ПИ, она видна только внутри list-comprehension-а.

Код, за который стыдно: 5 ошибок, которые нельзя допускать в Python