Обновить

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

Ага, если тысячи юзеров постоянно жмут не ту кнопку - надо менять кнопку, а не писать сотни мануалов о том, какую кнопку жать. Тем более что смысла делать именно такое поведение особо не видно. Это какая-то обрезанная разновидность статических переменных, но только для контейнеров? Могли бы просто запретить ее на уровне языка и все.

  1. Это не обычные юзеры, а программисты, которые себя считают умными и должны бы в состоянии документацию прочитать. Ну и на счёт сотни мануалов Вы преувеличиваете.

  2. Я считаю, что в любом ЯП есть тёмные области, поведение в которых выглядит странным. Про такие области надо знать и не ходить туда. Я думаю, что в принципе невозможно создать полноценный ЯП без "забавных особенностей".

  3. Вы предлагаете проверять мутабельность объекта? Здорово. Как кастомные объекты проверять будем? Обозначать их мутабельность руками? А если он обозначен как немутабельный, а на самом деле что-то изменяет? Вот на ровном месте проблемы создать можно.

  1. У программистов и так полно чего нужно держать в контексте. Забивать при этом голову еще и картой минных полей конкретного ЯП - излишнее усложнение. Насчет сотни не преувеличиваю. Только за последние месяцы этот злополучный param: list=[] встретился в разных каналах с десяток раз

  2. Но стремиться к их сокращению - стоит. В конце концов, бездушную машину приятнее нагружать, чем себя. К счастью, есть линтеры, которые ловят то, что не хочет ловить сам интерпретатор

  3. Есть же понятие хешируемости для применения в качестве ключей. Либо можно в принципе запретить любое значение дефолта, кроме элементарных базовых.

  1. Ну так то каналы. А в реальном коде? Там, где я встречал, можно было и без этого обойтись.

  2. Вот Вы сами решение и нашли. По мне так, это издержки того, что пишешь на многих языках. Ты знаешь либо один хорошо, либо много так себе. Электронные помощники спешат на помощь!

  3. Написать хеш метод - минутное дело. И это вообще ничего не говорит об мутабельности объекта. А на счёт базовых, в том то и дело, что они зашиты внутри, переменные выглядят для него одинаковыми. Да и если запретить, то мне что, нельзя использовать мною придуманый, особо точный Decimal(5/2)?

Там, где я встречал, можно было и без этого обойтись

Написать хеш метод - минутное дело. И это вообще ничего не говорит об мутабельности объекта

Всегда можно без этого, да, но речь не об этом. В моей картине мира подобные вещи - как открытый канализационный люк. Он должен быть всячески огорожен знаками, предупреждениями и заборами, чтобы исключить ненамеренное попадание (в контексте ЯП - не давать попасть туда, используя простые и типичные конструкции). Но если ты идешь туда целенаправленно, игнорируя знаки - предполагается, что ты знаешь, что делаешь, и готов к последствиям (в контексте ЯП - если ты задаешь дефолтное значение мутабельного объекта с переопределенным хешем, это явно не типичная штука, и тут уже сам несешь ответственность).

О, мы пришли к разному пониманию ЯП. По мне, так это инструмент: есть крутой, с отдельной камерой, автоматами аварийной защиты, двойной изоляцией и прочее. А есть простой, как скальпель, где пораниться самому проще, чем сделать что-то полезное. Ну и кучу всего между ними. И перед тем, как брать в руки инструмент, надо пройти ТБ: знать, на что он способен и как работать так, чтобы без рук не остаться.

ЯП все-таки скорее огромный и сложный инструмент, нежели скальпель)

Такую аналогию я в своё время читал про C. Мощный инструмент, но в неумелых руках больше проблем принесёт.

Думаю, если питон основной язык - то к особенностям привыкаешь и перестаёшь замечать,

Вот тут полностью соглашусь.

Касаемо примера. Может я уже адаптировался к написанию на Питоне, но как по мне, этот пример несколько синтетический. Я не очень представляю себе ситуацию, когда нам было бы без разницы, как обрабатывать данные, со списком или нет. Ну т.е. либо ты это хочешь вставить в конкретный контейнер, либо он тебе совсем не нужен. Единственное, что могу припомнить, так это рекурсивные функции. Но даже там лучше убрать дефолтное значение и передавать пустой список при граничных условиях. А в приведённом примере функция выполняет два действия: обработку значения и вставку его в контейнер. А такое надо разбивать на два. Если же нам нужны данные из контейнера (по типу последовательности Фибоначчи), так тут лучше свёртку использовать (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: x 0, lambda x: x 1 ...multipliers = [lambda x: x i for i in range(5)]# Проверяем. Ожидаем: 2 0 = 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)]

А вообще зря создатели в лямбдах не стали отделять параметры скобками, теряется читаемость

Я может неясно выразился, но мой поинт был в другом: не надо писать сложный код, когда есть код простой. Нафига мне помнить о том, как работает замыкание, если этого можно избежать. Проблема предложенного автором "правильного" кода ровно в том же, в чём и код изначальный: его действие неочевидно. Увы, но и Ваш код страдает тем же.

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

Согласен насчет не мучать мозг. Код не "мой", а просто вариант чуть улучшить нечитаемое. Я бы тоже сделал как выше, через обертку (create_handler(i))

зачем блистать умом, показывая свою "гениальность". пример тебе приведен для наглядности, чтобы ты не вникал в него несколько минут, отходя при этом от темы. все прекрасно понимают (я надеюсь) что писать такой код - это либо безумие, либо утонченный садизм.

Гениальность тут ни при чём. Просто откровенно говёный код предлагают заменить на чуть менее говёный код. Статья рассчитана на новичков, а значит они будут думать, что так - это нормально. Потом переучивай таких.

при чем здесь "говеный" или нет - 1. 2 - работа с асинхронностью для новичков? пупупу. откровенно тупые аргументы ;) 3 - спс что открыл для меня map-у, даже не знал, что так можно. 4 - бросать какахи в чей-то код и говорить: "смотри, вот так надо" и писать ... list(map(lambda ... какие курсы проходил?

Какой ты сложный.

  1. Где ты работу с асинхронностью увидел? 3-й случай? Так это база, в общем. Сейчас в каком-нибудь фреймворке легко её увидеть. Используешь асинхрощину - используй везде. Типично новичковая ошибка.

  2. Читай, что написано: Если нужно, то используй map. Опять же, если плохо написал, вот более развернутая мысль: вместо того, чтобы генерировать функции обработки, лучше применить единообразную обработку списка. Если такой возможности нет, то генерировать надо в явном виде через декораторами. Короче, если есть вариант обойтись без функций - обходимся без них. Если не вариант, то пишем в явном виде.

  3. Давай разбираться. map(lambda - типовая конструкция вообще-то. list мне нужен исключительно, чтобы в консоль данные вывести, ибо в оригинале там генератор. В цикле я бы его не использовал.

  4. Какахами я кидаюсь потому, что понимаю, что здесь идёт переход на тёмную сторону, так сказать. Где-то в книгах при обучении замыканиям, я видел такой пример. Но сейчас понимаю, что ТАК писать нельзя. Именно для того, чтобы не вспоминать, как там замыкания работают. Ибо ошибёшься гарантированно. Как выше отметили: типичный ногострел.

  5. Курсы проходил давно и книг прочёл тоже достаточно. Там и поваренная книга была, и ещё что-то.

в статье неточность:

Переменная i в глобальной области осталась висеть со значением 4. И все 5 ваших лямбд радостно лезут ...

i не сохраняется в глобальном ПИ, она видна только внутри list-comprehension-а.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации