Pull to refresh

Comments 47

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

асинхронность - это база

лень печатать почему это полный бред)

Какахами я кидаюсь потому, что понимаю, что здесь идёт переход на тёмную сторону

статья называется "Код, за который стыдно ...", что как бы намекает, как делать не стоит.

Курсы проходил давно

заметно

лень печатать почему это полный бред)

ну почему же? Там пример из типового фреймворке. Дали и работай. Я, когда начинал работать, тоже изучал все аспекты, мало ли что может пригодиться. Если для вас новичок - это школьник, который открывает для себя удивительный мир программирования, то это другой разговор. С одной стороны. С другой - тем важнее новичкам объяснить, в чём проблема. Учителя. боюсь, сами не в курсе.

статья называется "Код, за который стыдно ...", что как бы намекает, как делать не стоит.

И в качестве исправления приводят такой же плохой код. О чём я писал, кстати. Хотелось бы дискутировать с людьми, которые читать умеют хотя бы.

Хотелось бы дискутировать с людьми, которые читать умеют хотя бы.

Вот не могу не согласиться. Выше уже высказался по поводу "такой же плохой код":

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

async / await это не совсем база, скорее база+. "Новичок" в плане только знакомится с прогой (или 1 язык с поддержкой многопоточки / асинхронки).

Дай бог, чтобы учителя знали про ООП или SOLID (все таки не все учителя исключительно программисты)

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

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

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

Хорошая статья, только жалко, что сгенерирована ии.

Sign up to leave a comment.

Articles