Pull to refresh

Comments 57

Вопрос только один: зачем это так реализовано в Питоне? Так контринтуитивно для всех остальных языков. Следствие ли это того, что Питон изначально делался как простой скриптовый язык, и потом, для обеспечения обратной совместимости или старых внутренних болячек так была сделана передача параметров с дефолтный значением, или это сознательный выбор с самого начала и он даёт некие преимущества? Тогда какие?

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

Доки? Доки и учебники читают только лохи. Шутка. Но ваш вопрос не отменяет главного моего: зачем, почему именно так? Я всегда задаю этот вопрос, когда нахожу неочевидные, нелогичные (снаружи) решения.

Затем, что Гвидо Ван Россум 1)был плохим программистом, 2)ему очень хотелось сделать язык в котором всё не так как у других. Отсюта и табы, и неизменяемые объекты, и ловушки на ровном месте.

Есть гипотеза почему - Пайтон делался давно, следовательно, для программистов совсем другого уровня. Видно же - он катастрофически медленный не случайно, скоростью пожертвовали ради чего-то ещё. Чего именно - не скажу, но вижу - что бы оно ни было, сейчас уже не упёрлось - до аннотаций типа дожили. На варворов найденное в Риме не по назначению пользующих похоже, потому и открытия. Одно успокаивает - такое непотребство продолжалось только пока вода по акведукам текла...

Следствие ли это того, что Питон изначально делался как простой скриптовый язык

Python -- это мультпарадигмальный язык общего назначения. Он не создавался как "скриптовой язык".

или это сознательный выбор с самого начала и он даёт некие преимущества? Тогда какие?

Простота и последовательность реализации. Текущая реализация соответствует простым архитектурным решениям:

  1. Идентификаторы (имена) -- это ссылки на объекты.

  2. Аргумент функции -- это идентификатор.

  3. Все объекты делятся на изменяемые (mutable) и неизменяемые (immutable).

  4. Значения по умолчанию для аргументов функций и методов инициализируются один раз.

  5. Модификация изменяемого (mutable) объекта не требует инициализации нового объекта.

Значения по умолчанию для аргументов функций и методов инициализируются один раз.

Увидел ответы на множество вопросов, кроме моего: зачем/почему они инициализируются один раз, что противоречит практике в других языках? Какие преимущества даёт такой подход?

Так Вы ни разу не задали этот вопрос.

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

В этом Python не противоречит практике других языков

Тут далеко ходить не нужно. Тот же C++ ориентирован на value-семантику. Поэтому там значение по умолчанию не привязано к функции, а создаётся при каждом вызове. В примере ниже f1 и f2 - разные объекты.

std::vector<int> foo(std::vector<int> a = {}) {
  a.push_back(1);
  return a;
}

auto f1 = foo();
auto f2 = foo(); 

Но на C++ никто и не станет писать функции в стиле примера из статьи с передачей списка по значению для его модификации.

Потому что функция в Python - объект первого класса. Объявление функции с помощью оператора def создаёт этот объект и он начинает существовать в своей области видимости с проинициализированными значениями по-умолчанию.
При дальнейших вызовах этой функции используется этот самый ранее созданный объект функции. Так что тут как раз все очень даже логично.

Нет, не вижу логики в объяснении. С тем же успехом можно было написать не первого, в 18-го класса. Ниоткуда из объяснения не вытекает, почему дефолтные значения создаются 1 раз.

Нет, все равно нет логики. Из того, что функция создаётся один раз, совершенно не следует, что дефолтное значение параметров тоже должно создаватья один раз, а не при каждом вызове.

Когда появился питон практики многих других языков ещё не было

Скажите, а в каких языках можно сделать так?

x=[]
def foo(a=x): 
   a.append(1)

С точки зрения бытовой логики это ничем не отличается от

def foo(a=[]):
    a.append(1)

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

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

С точки зрения бытовой логики это ничем не отличается от

С точки зрения программистской логики это сильно отличается. В первом def мы присваиваем параметру дефолтную ссылку на некий объект. Во втором def мы создаём дефолтный объект и присваиваем его параметру.

В обоих случаях после = стоит выражение. Вопрос в какой момент оно выполняется? Захватывает ли оно переменные? Есть ли у него свой скоуп?

В питоне на эти вопросы ответили просто: выражение вычисляется в момент создания, соответственно захвата переменных не требуется, мы получаем результат вычисления и его используем. Если бы выражение считалось при каждом вызове, ему бы пришлось делать захват переменой x, что тоже не всем бы понравилось, плюс возник бы вопрос, можно ли в дефолте обращаться к другими переменным. Это тоже решение, но более сложное и со своими последствиями

Да, в идеале было бы запрещено писать def abc(a = x), но почему-то был выбран самый неочевидный вариант.

А сможете строго сформулировать что именно мы запрещаем?

Запретить использование ссылок на мутабельные объекты в качестве дефолтных значений для аргументов функций

В питоне нет деления на мутабельные и иммутабельные объекты. Это известно для нескольких встроенных типов и всё. В отличие от плюсов где есть const типы

К слову, функции и классы - мутабельные объекты

В питоне есть типы, объекты которых можно модифицировать, а есть типы, объекты которых модифицировать нельзя. Завести доп. флаг и хранить его на уровне PyType_Object - не самое сложное решение, правда?

А const - это не про типы, а про переменные

  1. Вы только что запретили использовать ссылку на функцию как дефолт

  2. Кортеж неизменяемый, а кортеж со списком - уже изменяемый

  3. Это сразу требует введения апи для того чтобы пользовательские классы помечать иммутабельными

  4. const T это буквально тип.

  5. Непонятно что делать есди в дефолте написано x+1, в какой момент его считать, в какой момент делать проверку.

Согласен, тут определенно есть ряд корнер-кейсов, с другой стороны - запретить использовать в кач-ве дефолтов хотя бы самые базовые кейсы: `[], {}, set()`, а такой кейс например как ([]) - настолько вычурный и около-нереальный, что его имхо можно не учитывать. Что касательно изменяемости функций: возможность навесить на объект функции новый атрибут в рантайме вообще никак не влияет на состояние объекта функции (я про code object). С класс-объектами да, тут сложнее конечно.

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

В целом нет большого смысла запрещать эти примитивные кейсы на уровне языка, линтер справится. Язык должен работать более универсально, особые случаи недостаточно особые чтобы они как-то отдельно влияли на язык.

нет уж, если можно менять, то можно менять.

И тем не менее, список unhashable, а функция вполне себе hashable. Т.е. разработчики языка когда то явно сделали такое вот разделение в рамках одной группы мутабельных объектов, ну потому что в одном случае мутабельность - это очень высокая гарантия сайд-эффектов, а в другом - просто свойство, которое есть

А при чем тут hashable? Изменяемые типы вполне могут быть хэшируемы. Зависит от того как мы определим равенство и хэш. Для многих типов равенство определено как проверка что этот тот же объект, в этом случае изменяемость ничего не ломает. Для других же типов равенство определяется на основе содержащихся там данных и поэтому хэшируемость невозможна если они меняются.

изменяемые объекты вполне могут быть хэшируемыми

Я об этом и сказал выше, на примере функций

в этом случае изменяемость ничего не ломает

Изменяемость функций-объектов вообще никогда и ничего не ломает))

поэтому хэшируемость невозможна

Правильнее сказать, что она запрещена. Именно из-за возможных сайд-эффектов которые бы были при отсутствии данного ограничения

Да и, банально, IDE / линтеры умеют идентифицировать мутабельные дефолты в аргументах

Не умеют. Только некоторые очевидные случаи

Очевидных случаев достаточно) Раз в пайтоне можно и некоторые «официально» иммутабельные объекты модифицировать (например, экземпляры frozen датаклассов/пайдентик моделей), то говорить о каком-то 100-процентном покрытии не приходится

Ппц. А ещё говорят php дно, в отличие от python.

Естественный вопрос

Естественный вопрос тут только один: о чём думал Гвидо ван Россум, создавая настолько контринтуитивное поведение, которое к тому же и с разными типами работает по разному? Наверное, о том же, о чём и Расмус Лердорф, когда добавлял знак доллара к именам переменных и создавал не всегда очевидный порядок аргументов или имён функций.

Разница между php и python только в том, что последний в своё время хорошо так популяризировал Google.

Поведение Python отличается от многих языков программирования. Однако, оно логично и последовательно в рамках принятых архитектурных решений.

В этом примере то же самое поведение:

a = []
b = a
a.append(1)
1 in b  # True

И в этом примере:

a = {}
b = a
a['key'] = 'value'
'key' in b  # True
b['key'] == 'value'  # True

Иллюстрация этой же "ловушки" Python на примере словаря:

call_counter = {'foo': 0, 'bar': 0}

def foo(x = call_counter):
    x['foo'] += 1

def bar(y = call_counter):
    y['bar'] += 1

foo()
foo()
bar()

call_counter['foo'] == 2  # True
call_counter['bar'] == 1  # True

Но ведь питон "простой" язык и многие рекомендуют начинать именно с него. Как же так?

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

Питон НЕ простой язык. Простой это С, Perl. По сравнению с еими питон сложный и нелогичный. Но успешный. Для программ - вызовов библиотек, алгоритмы на нём писать сложно.

Простота состоит в том, что многое скрыто и новичок может не задумываться. Но чем больше ты изучаешь, тем больше узнаешь особенностей.

в сообществе Python выработался стандарт (идиома), который считается единственно верным способом инициализации изменяемых аргументов

Кем выработался? И кем считается? Дайте ссылку на PEP, если возможно.

Вообще вариантов больше, и они зависят от конкретной ситуации.

  1. Самый радикальный - подумать еще раз. Возможно что интерфейс с опциональным параметром-коллекцией, изменяемой внутри функции, не самое лучшее решение.

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

    def add_item(item, storage=[]):
        storage = list(storage)
        storage.append(item)
        return storage
  3. Если опциональный параметр не предполагается менять (или он клонируется как в предыдущем примере):

    def log(message, additional_files=()):
        print(message)
        for f in additional_files:
          print(text, file=f)
    
  4. Если опциональный параметр не предполагается менять и это словарь:

    def log(message, additional_data=MappingProxyType({})):
        ...

Два последних варианта, помимо отсутствия условия на None, выполняют еще декларативную функцию - показывают что аргумент не изменяется внутри функции.

первый вариант не самый радикальный, а самый разумный, чистые функции - наше все :)

Проблема подхода в 2-х последних примерах VS дефолтного None в том, что в зависимости от типа (мутабельной) коллекции на вход вам нужно подобрать иммутабельный аналог этой коллекции для дефолта: list -> tuple, set -> frozenset, dict -> frozendict.

А для «декларативности» существуют аннотации типов :)

P.S. Никогда не понимал, зачем (даже опытные) разработчики пишут в сигнатуре функции например `x: list[str]`, когда можно написать `х: Sequence[str]` тем самым позволяя стороне, вызывающей функцию, передать в нее не только список, но и кортеж строк, при этом запрещая (формально) совершать в теле функции модифицирующие операции над х.Но да, это уже оффтоп и не про дефолты)

Статья классная, конечно.
Но эту информацию уже постили на Хабре несколько раз (и не только на Хабре).
Поэтому для себя нового ничего не открыл.

Давеча на Хабре была статья что list в питоне - это, оказывается, динамический массив! А я то всю жизнь думал, что это linked list (сарказм)

Всегда знал, что питон - всратый, бесполезный язык.

Как повезло, что в питоне есть этот архитектурный баг: [ ]! Теперь эту тему можно мусолить миллион раз на радость "ура, в питоне есть проблема, значит это перекрывает все мои мучения с моим языком". Реально миллионный раз. Каждое второе собеседование включает этот прикол, уже не баг, а прикол

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

В данном примере функция добавления элемента в массив может выглядеть примерно так:

Function AddItem (byVal Item, byRef kol_items, byRef Arr(), byRef error) as Boolean

/// Функция возвращает True если операция выполнена успешно, False если не выполнена и в еггог возвращается код ошибки. Добавляемый элемент Item объявлен как входной параметр передающийся значением. Остальные параметры объявлены как ссылки (адреса) на переменные обьявленные в вызывающей процедуре: текущее кол-во элементов в массиве, сам массив элементов и код возвращаемой ошибки. Причём текущее значение кол-ва элементов в массиве kol_items является одновременно и входным и выходным параметром передаваемые в функцию по ссылке. ///

AddItem = False

error = 0

If kol_items < 0 then error=1 Exit Function EndIf ///выход при отрицательном kol_items///

kol_items = kol_items + 1

AddDimention Arr, kol_items ///увеличение размерности массива до kol_items

Arr (kol_items) = Item ///непосредственное добавление нового элемента

AddItem = True ///успешно завершение функции

End Function

Здесь AddDimention есть оператор или функция ЯП который(ая) увеличивает текущую размерность массива до заданного значения при сохранении уже имеющихся в нем элементов. Нумерация элементов массива логично начинается с 1. В принципе можно обойтись и БЕЗ error, но в этом случае вызывающая процедура должна сама убедиться в добавлении элемента, сравнив отправленное значение kol_items с полученным. Если оно увеличилось на 1, то добавление прошло успешно. Но для соблюдения стиля надёжного программирования, любая процедура (функция), помимо обязательной проверки значений входных параметров на корректность, должна возвращать код ошибки (лучше вместе с текстом) возникающей при её выполнении.

Я в очередной раз вспоминаю, почему я не люблю Python, JavaScript и другие подобные языки. Когда указатели скрываются от программиста якобы для какого-то "удобства" и "простоты" - это медвежья услуга. Они настолько стесняются слова "указатель", что даже заменили его на "ссылка". В результате, вместо того, чтобы прямо из кода понять, что происходит, надо изучать кучу неявных правил, рассказывающих тебе о том, когда переменная передается по ссылке, а когда по значению. И именно сама эта концепция привела к тому, что этот "баг" вообще появился. Явное всегда лучше неявного, коллеги!

Что самое интересное, указатели скрыты. Но во всех случаях когда надо объяснить неочевидное поведение, приходится эти указатели вытаскивать наружу и без этого объяснить не получается.

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

Числа и строки передаются ровно так же как все другие объекты.

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

Да нет никакого деления между передачей строки и других объектов. Они просто передаются. Нет никакой магии. Строки и числа просто не имеют методов изменения их внутреннего состояния.

Потому что в пайтоне все объекты. И людям проще рассматривать неизменяемость-изменяемость типа как раз в парадигме передается по ссылке или по значению. Это упрощение, но иначе будем видеть такие комментарии "как все у вас в пайтоне сложно"

Я не знаю кому это проще. Очень много каши в голове от такого объяснения. Неизменяемость это просто отсутствие способа изменить. Передача по значению важна когда у вас есть изменяемость, просто она не затрагивает оригинальную копию

Да-да. Сначала напишем кучу багов из-за неявных указателей. Потом напишем кучу багов из-за проблемы Mutable Default Arguments, описанной в статье. Потом напишем кучу багов из-за оператора is в сравнении с == и кэширования объектов. И т.д. и т.п. Но так-то всё просто, да. Очень просто написать баг.

Python хорош как точка сбора всех классных датасатанистских библиотек. Но как язык программирования... Увольте...

В питоне всегда объекты передаются по копии указтеля. Нет никаких "когда"

Что значит копия указателя? Указатель это по сути число, копия указателя это копия числа?

Во-во. Объекты... И это надо знать назубок, что скалярные переменные передаются по копии, а всякие разные объекты по "ссылке". И сидит такой начинающий программист и не врубается, почему же tuple, вроде бы не число и не строка, а всё равно передаётся по значению. И таких вот правил из разных разделов в динамических языках накапливается так много, что потом из-за банальной невнимательности появляются баги, которые можно было бы избежать ещё на этапе компиляции в других языках.

Sign up to leave a comment.

Articles