«Наименьшее удивление» и изменчивый аргумент по умолчанию


2597

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

def foo(a=[]):
    a.append(5)
    return a

Python послушники бы ожидать эта функция всегда возвращает список только с одним элементом: [5]. Результат вместо этого очень отличается, и очень удивительно (для новичка):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Один мой менеджер однажды познакомился с этой функцией и назвал ее «драматическим недостатком дизайна» языка. Я ответил, что поведение имело под собой объяснение, и это действительно очень удивительно и неожиданно, если вы не понимаете внутренностей. Однако я не смог ответить (сам себе) на следующий вопрос: в чем причина привязки аргумента по умолчанию при определении функции, а не при выполнении функции? Я сомневаюсь, что опытное поведение имеет практическое применение (кто на самом деле использовал статические переменные в C, без выявления ошибок?)

Редактировать :

Бачек сделал интересный пример. Вместе с большинством ваших комментариев и комментариев Утаала, в частности, я уточнил:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Мне кажется, что проектное решение было относительно того, куда поместить область параметров: внутри функции или «вместе» с ней?

Выполнение привязки внутри функции будет означать, что xона эффективно привязана к указанному значению по умолчанию, когда функция вызывается, а не определяется, что может представлять собой серьезный недостаток: defлиния будет «гибридной» в том смысле, что часть привязки (из объект функции) будет происходить при определении, а часть (назначение параметров по умолчанию) - во время вызова функции.

Фактическое поведение является более последовательным: все, что в этой строке, оценивается при выполнении этой строки, то есть при определении функции.



4
Я не сомневаюсь, что изменчивые аргументы нарушают принцип наименьшего удивления для обычного человека, и я видел, как новички вступают туда, а затем героически заменяют списки рассылки кортежами рассылки. Тем не менее изменчивые аргументы все еще соответствуют Python Zen (Pep 20) и попадают в предложение «очевидное для голландского» (понимаемое / используемое программистами Python). Рекомендуемый обходной путь со строкой документа является наилучшим, но в настоящее время устойчивость к строкам документа и любым (написанным) документам не так уж редка. Лично я бы предпочел декоратор (скажем, @fixed_defaults).
Серж

5
Мой аргумент, когда я сталкиваюсь с этим: «Зачем вам нужно создавать функцию, которая возвращает изменяемый файл, который может быть изменяемым, который вы могли бы передать функции? Либо он изменяет изменяемый, либо создает новый. сделать оба с одной функцией? И почему интерпретатор должен быть переписан, чтобы позволить вам сделать это без добавления трех строк в ваш код? " Потому что здесь мы говорим о переписывании способа, которым интерпретатор обрабатывает определения функций и их вызовы. Это очень много для крайне необходимого варианта использования.
Алан

12
Msgstr "Новички в Python ожидают, что эта функция будет всегда возвращать список только с одним элементом:. [5]" Я новичок в Python, и я не ожидал бы этого, потому что, очевидно foo([1]), вернется [1, 5], а не [5]. Что вы хотели сказать, так это то, что новичок ожидал, что функция, вызываемая без параметра , всегда будет возвращаться [5].
симплектоморфный

2
Этот вопрос задает вопрос: «Почему этот [неправильный путь] был реализован так?» Это не спрашивает, "Каков правильный путь?" , который описан в [ Почему использование arg = None устраняет проблему изменяемого аргумента Python по умолчанию? ] * ( stackoverflow.com/questions/10676729/… ). Новые пользователи почти всегда меньше интересуются первым и гораздо больше вторым, так что иногда это очень полезная ссылка / обман.
smci

Ответы:


1614

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

Как только вы начинаете думать об этом, тогда это полностью имеет смысл: функция - это объект, оцениваемый по его определению; Параметры по умолчанию являются своего рода «данными члена», и поэтому их состояние может меняться от одного вызова к другому - точно так же, как и в любом другом объекте.

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


80
Всем, кто читает вышеуказанный ответ, я настоятельно рекомендую вам потратить время на прочтение связанной статьи Effbot. Как и вся другая полезная информация, очень удобно узнать, как использовать эту языковую функцию для кэширования / запоминания результатов!
Кэм Джексон

85
Даже если это объект первого класса, можно по-прежнему представлять проект, в котором код для каждого значения по умолчанию сохраняется вместе с объектом и переоценивается при каждом вызове функции. Я не говорю, что это было бы лучше, просто функции, являющиеся первоклассными объектами, не полностью исключают это.
Gerrit

313
Извините, но все, что считается «самым большим WTF в Python», определенно является недостатком дизайна . Это является источником ошибок для всех в какой-то момент, потому что никто не ожидает такого поведения вначале - что означает, что его не следовало разрабатывать таким образом с самого начала. Мне все равно, через какие скачки они должны были прыгнуть, они должны были спроектировать Python так, чтобы аргументы по умолчанию были нестатичными.
BlueRaja - Дэнни Пфлюгофт

192
Независимо от того, является ли это недостатком проекта, ваш ответ, по-видимому, подразумевает, что такое поведение является каким-то необходимым, естественным и очевидным, поскольку функции являются первоклассными объектами, а это просто не тот случай. Python имеет замыкания. Если вы замените аргумент по умолчанию присваиванием в первой строке функции, он будет оценивать выражение при каждом вызове (возможно, с использованием имен, объявленных во вложенной области видимости). Нет никакой причины, по которой было бы невозможно или разумно оценивать аргументы по умолчанию каждый раз, когда функция вызывается точно таким же образом.
Марк Амери

24
Дизайн не следует непосредственно из functions are objects. В вашей парадигме предложение состоит в том, чтобы реализовать значения функций по умолчанию в качестве свойств, а не атрибутов.
Букзор

273

Предположим, у вас есть следующий код

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Когда я вижу объявление о том, что есть, наименее удивительным является мысль о том, что, если первый параметр не задан, он будет равен кортежу. ("apples", "bananas", "loganberries")

Однако, как предполагается позже в коде, я делаю что-то вроде

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

тогда, если бы параметры по умолчанию были связаны при выполнении функции, а не объявлении функции, то я был бы изумлен (очень плохим способом), чтобы обнаружить, что фрукты были изменены. Это было бы более поразительным IMO, чем обнаружение того, что ваша fooфункция выше меняла список.

Настоящая проблема заключается в изменчивых переменных, и все языки имеют эту проблему в некоторой степени. Вот вопрос: предположим, в Java у меня есть следующий код:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Теперь, моя карта использует значение StringBufferключа, когда он был помещен в карту, или он хранит ключ по ссылке? В любом случае, кто-то удивлен; либо человек, который пытался вытащить объект, Mapиспользуя значение, идентичное тому, с которым они его поместили, либо человек, который, кажется, не может извлечь свой объект, даже если ключ, который они используют, буквально является тем же объектом это использовалось для помещения его в карту (именно поэтому Python не позволяет использовать его изменяемые встроенные типы данных в качестве ключей словаря).

Ваш пример - хороший случай, когда новички в Python будут удивлены и укушены. Но я бы сказал, что если бы мы «исправили» это, то это только создало бы другую ситуацию, когда их укусили бы, а эта была бы еще менее интуитивной. Более того, это всегда имеет место при работе с изменяемыми переменными; Вы всегда сталкиваетесь со случаями, когда кто-то может интуитивно ожидать того или иного поведения в зависимости от того, какой код он пишет.

Мне лично нравится текущий подход Python: аргументы функции по умолчанию оцениваются, когда функция определена, и этот объект всегда является значением по умолчанию. Я полагаю, что они могли бы использовать особый случай, используя пустой список, но такой особый случай вызвал бы еще большее удивление, не говоря уже о несовместимости в обратном направлении.


30
Я думаю, что это вопрос дебатов. Вы действуете по глобальной переменной. Любая оценка, выполняемая в любом месте вашего кода с использованием вашей глобальной переменной, теперь будет (правильно) называться («черника», «манго»). параметр по умолчанию может быть как любой другой случай.
Стефано Борини

47
На самом деле, я не думаю, что согласен с вашим первым примером. Я не уверен, что мне нравится идея изменить инициализатор подобным образом, но если бы я это сделал, я бы ожидал, что он будет вести себя точно так, как вы описываете - изменение значения по умолчанию на ("blueberries", "mangos").
Бен Бланк

12
Параметр по умолчанию является , как и любой другой случай. Что неожиданно, так это то, что параметр является глобальной переменной, а не локальной. Что, в свою очередь, объясняется тем, что код выполняется при определении функции, а не при вызове. Как только вы это получите, и то же самое относится и к занятиям, это совершенно ясно.
Леннарт Регебро

17
Я нахожу пример вводящим в заблуждение, а не блестящим. Если some_random_function()дописывает fruitsвместо присвоения ему поведение eat() будет меняться. Так много для нынешнего замечательного дизайна. Если вы используете аргумент по умолчанию, на который ссылаются в другом месте, а затем изменяете ссылку извне функции, вы напрашиваетесь на неприятности. Настоящий WTF - это когда люди определяют свежий аргумент по умолчанию (литерал списка или вызов конструктора) и все еще получают бит.
Алексис

13
Вы просто явно объявили globalи переназначили кортеж - нет ничего удивительного, если eatпосле этого все будет работать иначе.
user3467349

241

Соответствующая часть документации :

Значения параметров по умолчанию оцениваются слева направо при выполнении определения функции. Это означает, что выражение вычисляется один раз, когда функция определена, и что для каждого вызова используется одно и то же «предварительно вычисленное» значение. Это особенно важно понимать, когда параметр по умолчанию является изменяемым объектом, таким как список или словарь: если функция изменяет объект (например, путем добавления элемента в список), значение по умолчанию фактически изменяется. Это вообще не то, что было задумано. Способ обойти это - использовать Noneпо умолчанию и явно проверить его в теле функции, например:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

181
Фразы «это вообще не то, что было задумано» и «способ обойти это» пахнут, как будто они документируют недостаток дизайна.
Букзор

4
@ Мэтью: я хорошо знаю, но это не стоит ловушки. Как правило, по этой причине вы, как правило, видите, что руководства по стилю и линтеры помечают изменяемые значения по умолчанию как неправильные Явный способ сделать то же самое - вставить атрибут в функцию ( function.data = []) или, что еще лучше, создать объект.
Букзор

6
@bukzor: Подводные камни должны быть отмечены и задокументированы, поэтому этот вопрос хорош и получил так много положительных отзывов. В то же время, ловушки не обязательно должны быть удалены. Сколько новичков в Python передали список функции, которая его изменила, и были шокированы, увидев, что изменения отображаются в исходной переменной? Тем не менее, изменчивые типы объектов замечательны, когда вы понимаете, как их использовать. Я предполагаю, что это просто сводится к мнению об этой конкретной ловушке.
Мэтью

33
Фраза «это вообще не то, что было задумано» означает «не то, что на самом деле хотел сделать программист», а не «не то, что должен делать Python».
holdenweb

4
@holdenweb Ого, я опоздал на вечеринку. Учитывая контекст, букзор совершенно прав: они документируют поведение / последствия, которые не были «предназначены», когда они решили, что язык должен выполнить определение функции. Так как это непреднамеренное последствие их выбора дизайна, это недостаток дизайна. Если бы это не было недостатком дизайна, не было бы даже необходимости предлагать «способ обойти это».
code_dredd

118

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

При условии, что объекты Python изменчивы, я думаю, что это следует учитывать при разработке аргументов по умолчанию. Когда вы создаете экземпляр списка:

a = []

вы ожидаете получить новый список, на который ссылается a.

Почему a=[]в

def x(a=[]):

создать новый список по определению функции, а не по вызову? Это как если бы вы спросили: «Если пользователь не предоставит аргумент, создайте новый список и используйте его так, как если бы он был создан вызывающей стороной». Я думаю, что это двусмысленно:

def x(a=datetime.datetime.now()):

пользователь, хотите ли вы aпо умолчанию установить дату и время, соответствующие определению или выполнению x? В этом случае, как и в предыдущем, я буду придерживаться того же поведения, как если бы аргумент по умолчанию «назначение» был первой инструкцией функции ( datetime.now()вызываемой при вызове функции). С другой стороны, если пользователь хочет отображать время определения, он может написать:

b = datetime.datetime.now()
def x(a=b):

Я знаю, я знаю: это закрытие. В качестве альтернативы Python может предоставить ключевое слово для принудительного связывания во время определения:

def x(static a=b):

11
Вы можете сделать: def x (a = None): И затем, если a None, установите a = datetime.datetime.now ()
Anon

20
Спасибо тебе за это. Я не мог понять, почему это бесит меня. Вы сделали это красиво с минимумом нечеткости и путаницы. Как кто-то, пришедший из системного программирования на C ++ и иногда наивно «переводящий» языковые функции, этот ложный друг сильно удивил меня, как атрибуты класса. Я понимаю, почему все так, но я не могу не любить это, независимо от того, что из этого может получиться положительного. По крайней мере, это настолько противоречит моему опыту, что я, вероятно, (надеюсь) никогда этого не забуду ...
AndreasT

5
@ Андреас, как только вы используете Python достаточно долго, вы начинаете понимать, как логично для Python интерпретировать вещи как атрибуты класса так, как он это делает - это только из-за специфических особенностей и ограничений языков, таких как C ++ (и Java, и C # ...) что имеет смысл class {}интерпретировать содержимое блока как принадлежащее экземплярам :) Но когда классы являются первоклассными объектами, очевидно, что их содержимое (в памяти) естественным образом отражает их содержимое (в коде).
Карл Кнехтель

6
Нормативная структура не является причудой или ограничением в моей книге. Я знаю, что это может быть неуклюжим и безобразным, но вы можете назвать это «определением» чего-либо. Динамические языки кажутся мне немного похожими на анархистов: конечно, все свободны, но вам нужна структура, чтобы кто-то мог очистить мусор и проложить дорогу. Думаю, я старый ... :)
AndreasT

4
Функция определения выполняется в модуле времени загрузки. Тело функции выполняется во время вызова функции. Аргумент по умолчанию является частью определения функции, а не тела функции. (Это становится более сложным для вложенных функций.)
Lutz Prechelt

84

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

Сравните это:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

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

Это просто «Как это работает», и заставить его работать по-другому в случае функции, вероятно, будет сложно, а в случае класса, вероятно, невозможно, или, по крайней мере, сильно замедлить создание экземпляров объекта, так как вам придется держать код класса вокруг и выполнить его при создании объектов.

Да, это неожиданно. Но как только копейка падает, она отлично вписывается в общую работу Python. На самом деле, это хорошее учебное пособие, и как только вы поймете, почему это происходит, вы станете гораздо лучше питонить.

Тем не менее, это должно быть заметно в любом хорошем учебнике по Python. Потому что, как вы упоминаете, все сталкиваются с этой проблемой рано или поздно.


Как вы определяете атрибут класса, который отличается для каждого экземпляра класса?
Kieveli

19
Если это отличается для каждого экземпляра, это не атрибут класса. Атрибуты класса являются атрибутами класса. Отсюда и название. Следовательно, они одинаковы для всех случаев.
Леннарт Регебро

1
Как вы определяете атрибут в классе, который отличается для каждого экземпляра класса? (Переопределено для тех, кто не может определить, что человек, не знакомый с соглашениями об именах Python, может задавать вопрос о нормальных переменных-членах класса).
Кьевели

@Kievieli: Вы говорите о нормальных переменных-членах класса. :-) Вы определяете атрибуты экземпляра, говоря self.attribute = value в любом методе. Например, __init __ ().
Леннарт Регебро

@Kieveli: Два ответа: вы не можете, потому что любая вещь, которую вы определяете на уровне класса, будет атрибутом класса, а любой экземпляр, который обращается к этому атрибуту, будет иметь доступ к одному и тому же атрибуту класса; Вы можете / sort of /, используя propertys - которые на самом деле являются функциями уровня класса, которые действуют как обычные атрибуты, но сохраняют атрибут в экземпляре вместо класса (используя, self.attribute = valueкак сказал Леннарт).
Итан Фурман

66

Почему бы тебе не заняться самоанализом?

Я действительно удивлен, что никто не выполнил проницательный самоанализ, предложенный Python ( 2и 3применить) в отношении вызываемых.

Учитывая простую маленькую функцию, funcопределенную как:

>>> def func(a = []):
...    a.append(5)

Когда Python встречает его, первое, что он сделает, это скомпилирует его, чтобы создать codeобъект для этой функции. Пока этот шаг компиляции сделан, Python оценивает * и затем сохраняет параметры по умолчанию (здесь пустой список []) в самом объекте функции . Как отмечается в верхнем ответе: список aтеперь можно считать членом функции func.

Итак, давайте сделаем некоторый самоанализ, до и после, чтобы проверить, как список расширяется внутри объекта функции. Я использую Python 3.xдля этого, для Python 2 применяется то же самое (используйте __defaults__или func_defaultsв Python 2; да, два имени для одной и той же вещи).

Функция перед выполнением:

>>> def func(a = []):
...     a.append(5)
...     

После того, как Python выполнит это определение, он возьмет любые заданные по умолчанию параметры ( a = []здесь) и поместит их в __defaults__атрибут для объекта функции (соответствующий раздел: Callables):

>>> func.__defaults__
([],)

Итак, пустой список как одна запись __defaults__, как и ожидалось.

Функция после выполнения:

Давайте теперь выполним эту функцию:

>>> func()

Теперь давайте посмотрим на них __defaults__снова:

>>> func.__defaults__
([5],)

Изумленные? Значение внутри объекта меняется! Последовательные вызовы функции теперь просто добавляются к этому внедренному listобъекту:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Итак, у вас есть причина, по которой возникает этот «недостаток» , потому что аргументы по умолчанию являются частью объекта функции. Здесь нет ничего странного, все это немного удивляет.

Распространенным решением для борьбы с этим является использование Noneпо умолчанию, а затем инициализация в теле функции:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

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


Для дальнейшей проверки того, что список в списке __defaults__совпадает с используемым в функции, funcвы можете просто изменить свою функцию, чтобы она возвращала idсписок, aиспользуемый внутри тела функции. Затем сравните его со списком в __defaults__(position [0]in __defaults__), и вы увидите, как они действительно относятся к одному и тому же экземпляру списка:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Все с силой самоанализа!


* Чтобы убедиться, что Python оценивает аргументы по умолчанию во время компиляции функции, попробуйте выполнить следующее:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

как вы заметите, input()вызывается до того, как будет создан процесс построения функции и привязки ее к имени bar.


1
Требуется ли id(...)для последней проверки, или isоператор ответил бы на тот же вопрос?
Das-G

1
С @ das-g все isбудет в порядке, я просто использовал, id(val)потому что думаю, что он может быть более интуитивным.
Димитрис Фасаракис Хиллиард

Использование Noneпо умолчанию серьезно ограничивает полезность __defaults__самоанализа, поэтому я не думаю, что это работает как защита от __defaults__работы так, как работает. Ленивая оценка сделала бы больше, чтобы сохранить функциональные значения по умолчанию полезными с обеих сторон.
Brilliand

58

Раньше я думал, что создание объектов во время выполнения будет лучшим подходом. Сейчас я менее уверен, так как вы теряете некоторые полезные функции, хотя это может стоить того, чтобы просто не допустить путаницы новичка. Недостатки этого:

1. Производительность

def foo(arg=something_expensive_to_compute())):
    ...

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

2. Принудительное связывание параметров

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

funcs = [ lambda i=i: i for i in range(10)]

Это возвращает список функций, которые возвращают 0,1,2,3 ... соответственно. Если поведение будет изменено, они вместо этого будут привязаны iк значению времени вызова i, так что вы получите список функций, которые все вернули 9.

Единственный способ реализовать это иначе - создать дальнейшее замыкание с привязкой i, т.е.

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Самоанализ

Рассмотрим код:

def foo(a='test', b=100, c=[]):
   print a,b,c

Мы можем получить информацию об аргументах и ​​значениях по умолчанию, используя inspectмодуль, который

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Эта информация очень полезна для таких вещей, как генерация документов, метапрограммирование, декораторы и т. Д.

Теперь предположим, что поведение значений по умолчанию можно изменить так, чтобы это было эквивалентно:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

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


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

@SilentGhost: я говорю о том, было ли изменено поведение, чтобы воссоздать его - создание его один раз является текущим поведением, и почему существует изменяемая проблема по умолчанию.
Брайан

1
@yairchu: Это предполагает, что конструкция безопасна для этого (т.е. не имеет побочных эффектов). Самоанализ аргументов не должен ничего делать , но оценка произвольного кода вполне может дать эффект.
Брайан

1
Различный языковой дизайн часто просто означает писать вещи по-другому. Ваш первый пример может быть легко написан как: _exорого = дорогой (); def foo (arg = _exорого), если вы не хотите, чтобы он был переоценен.
Гленн Мейнард

@Glenn - это то, что я имел в виду под «кэшированием внешней переменной» - это немного более многословно, и в результате вы получите дополнительные переменные в своем пространстве имен.
Брайан

55

5 очков в защиту Python

  1. Простота : поведение простое в следующем смысле: большинство людей попадают в эту ловушку только один раз, а не несколько раз.

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

  3. Полезность : как Фредерик Лунд указывает в своем объяснении «Значения параметров по умолчанию в Python» , текущее поведение может быть весьма полезным для продвинутого программирования. (Используйте экономно.)

  4. Достаточная документация : в самой базовой документации Python, учебнике, проблема громко объявлена ​​как «Важное предупреждение» в первом подразделе Раздела «Подробнее об определении функций» . В предупреждении даже используется жирный шрифт, который редко применяется за пределами заголовков. RTFM: прочитайте прекрасное руководство.

  5. Мета-обучение : Попадание в ловушку на самом деле очень полезный момент (по крайней мере, если вы учитесь размышлять), потому что впоследствии вы лучше поймете пункт «Согласованность» выше, и это многому вас научит в Python.


18
Мне потребовался год, чтобы обнаружить, что такое поведение портит мой код на производстве, и в итоге я удалил полную функцию, пока я случайно не наткнулся на этот недостаток дизайна. Я использую Джанго. Поскольку в промежуточной среде не было много запросов, эта ошибка никогда не влияла на QA. Когда мы начали работу и получили много одновременных запросов - некоторые служебные функции начали перезаписывать параметры друг друга! Делаем дыры в безопасности, ошибки, а что нет.
Ориадам

7
@oriadam, без обид, но мне интересно, как ты выучил Python, не сталкиваясь с этим раньше. Я только сейчас изучаю Python, и эта возможная ловушка упоминается в официальном руководстве по Python вместе с первым упоминанием аргументов по умолчанию. (Как упомянуто в пункте 4 этого ответа.) Я полагаю, что мораль - довольно не сочувственно - читать официальные документы языка, который вы используете для создания производственного программного обеспечения.
Подстановочный

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

52

Такое поведение легко объяснить:

  1. объявление функции (класса и т. д.) выполняется только один раз, создавая все объекты значений по умолчанию
  2. все передается по ссылке

Так:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a не изменяется - каждый вызов присваивания создает новый объект int - печатается новый объект
  2. b не изменяется - новый массив строится из значения по умолчанию и печатается
  3. c изменения - операция выполняется над тем же объектом - и она печатается

(На самом деле, add - плохой пример, но целочисленность неизменяемости по-прежнему является моей главной точкой.)
Anon

Осознал это к моему огорчению после проверки того, что с b, установленным в [], b .__ add __ ([1]) возвращает [1], но также оставляет b все еще [], даже если списки изменчивы. Виноват.
Anon

@ ANon: есть __iadd__, но он не работает с int. Конечно. :-)
Veky

35

То, что вы спрашиваете, почему это:

def func(a=[], b = 2):
    pass

внутренне не эквивалентно этому:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

за исключением случая явного вызова func (None, None), который мы проигнорируем.

Другими словами, вместо оценки параметров по умолчанию, почему бы не сохранить каждый из них и не оценить их при вызове функции?

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


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

10
Да, но это все равно замедлит Python, и это будет довольно удивительно, если вы не сделаете то же самое для определений классов, что сделает его тупо медленным, поскольку вам придется перезапускать полное определение класса каждый раз, когда вы создаете экземпляр учебный класс. Как уже упоминалось, исправление будет более удивительным, чем проблема.
Леннарт Регебро

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

6
Изменить его сейчас было бы безумием - мы просто исследуем, почему это так. Если бы он выполнил позднюю оценку по умолчанию для начала, это не обязательно было бы удивительно. Это определенно верно, что такое основное различие в синтаксическом анализе будет иметь широкие, и, вероятно, многие неясные последствия для языка в целом.
Гленн Мейнард

35

1) Так называемая проблема «изменяемого аргумента по умолчанию» в целом является особым примером, демонстрирующим, что:
«Все функции с этой проблемой страдают также от аналогичной проблемы побочных эффектов для фактического параметра ».
то есть против правил функционального программирования, обычно нежелательны и должны быть исправлены вместе.

Пример:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Решение : а копия
Абсолютно безопасное решение является copyили deepcopyвходной объект первым , а затем делать все , что с копией.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Многие встроенные изменяемые типы имеют метод копирования, например some_dict.copy()или, some_set.copy()или могут быть скопированы так же, как somelist[:]или list(some_list). Каждый объект также может быть скопирован copy.copy(any_object)или более тщательно copy.deepcopy()(последний полезен, если изменяемый объект составлен из изменяемых объектов). Некоторые объекты в основном основаны на побочных эффектах, таких как «файловый» объект, и не могут быть воспроизведены путем копирования. копирование

Пример задачи для аналогичного вопроса SO

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

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

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

2)
Только если побочный эффект для фактического параметра требуется, но нежелателен для параметра по умолчанию, тогда полезным решением является def ...(var1=None): if var1 is None: var1 = [] Подробнее.

3) В некоторых случаях полезно изменяемое поведение параметров по умолчанию .


5
Надеюсь, вы знаете, что Python не является функциональным языком программирования.
Veky

6
Да, Python - это мультипарагигмальный язык с некоторыми функциональными возможностями. («Не делайте каждую проблему похожей на гвоздь только потому, что у вас есть молоток».) Многие из них соответствуют лучшим практикам Python. У Python есть интересное HOWTO Функциональное программирование. Другие функции - это замыкания и каррирование, которые здесь не упоминаются.
hynekcer

1
На этом позднем этапе я бы также добавил, что семантика присваивания Python была разработана специально, чтобы избежать копирования данных там, где это необходимо, поэтому создание копий (и особенно глубоких копий) отрицательно повлияет как на время выполнения, так и на использование памяти. Поэтому их следует использовать только в случае необходимости, но новичкам часто трудно понять, когда это так.
holdenweb

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

Я согласен с этим ответом. И я не понимаю, почему def f( a = None )конструкция рекомендуется, когда вы действительно имеете в виду что-то еще. Копирование - это нормально, потому что вы не должны изменять аргументы. И когда вы это делаете if a is None: a = [1, 2, 3], вы все равно копируете список.
Коддо

30

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

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

В этом коде нет значений по умолчанию, но вы получите точно такую ​​же проблему.

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

Ваш оригинал foo с аргументом по умолчанию не должен изменятьсяa , был ли он передан явно или получил значение по умолчанию. Ваш код должен оставлять изменяемые аргументы в покое, если из контекста / имени / документации не ясно, что аргументы должны быть изменены. Использование изменяемых значений, передаваемых в качестве аргументов в качестве локальных временных файлов, является крайне плохой идеей, независимо от того, находимся ли мы в Python или нет, и задействованы ли аргументы по умолчанию или нет.

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


7
Хотя это связано, я думаю, что это отличное поведение (как мы ожидаем, appendчтобы изменить a"на месте"). То, что изменяемая по умолчанию переменная не восстанавливается при каждом вызове, является "неожиданным" битом ... по крайней мере для меня. :)
Энди Хейден

2
@AndyHayden, если ожидается, что функция изменяет аргумент, зачем иметь значение по умолчанию?
Марк Рэнсом

@MarkRansom единственный пример, который я могу придумать cache={}. Тем не менее, я подозреваю, что возникает «наименьшее удивление», когда вы не ожидаете (или не хотите) функцию, которую вы вызываете, чтобы изменить аргумент.
Энди Хейден

1
@AndyHayden Я оставил свой собственный ответ здесь с расширением этого чувства. Дайте мне знать, что вы думаете. Я мог бы добавить ваш пример cache={}в это для полноты.
Марк Рэнсом

1
@AndyHayden Суть моего ответа в том, что если вы когда-нибудь удивитесь, случайно изменив значение аргумента по умолчанию, то у вас есть еще одна ошибка, заключающаяся в том, что ваш код может случайно изменить значение вызывающего, когда значение по умолчанию не использовалось. И обратите внимание, что использование Noneи присвоение истинного значения по умолчанию, если arg is None , не решает эту проблему (по этой причине я считаю это анти-паттерном). Если вы исправите другую ошибку, избегая изменяющихся значений аргументов, независимо от того, имеют ли они значения по умолчанию, тогда вы никогда не заметите и не позаботитесь об этом «удивительном» поведении.
Бен

27

Тема уже занята, но из того, что я прочитал здесь, следующее помогло мне понять, как это работает внутри:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
на самом деле это может быть немного запутанным для новичков, так как a = a + [1]перегрузки a... рассмотрите возможность изменить его b = a + [1] ; print id(b)и добавить строку a.append(2). Это сделает более очевидным, что +в двух списках всегда создается новый список (назначенный b), в то время как измененный aможет иметь то же самое id(a).
Йорн Хис

25

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

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Я дам вам подсказку. Вот разборка (см. Http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Я сомневаюсь, что опытное поведение имеет практическое применение (кто на самом деле использовал статические переменные в C, без выявления ошибок?)

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


24

Python: изменяемый аргумент по умолчанию

Аргументы по умолчанию оцениваются во время компиляции функции в объект функции. При использовании этой функции несколько раз этой функцией они являются и остаются одним и тем же объектом.

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

Они остаются мутированными, потому что они - один и тот же объект каждый раз.

Эквивалентный код:

Так как список привязан к функции, когда объект функции компилируется и создается, это:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

почти точно эквивалентно этому:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

демонстрация

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

  • видя, что список создан до того, как функция завершила компиляцию в объект функции,
  • наблюдая, что идентификатор одинаков при каждом обращении к списку,
  • наблюдая, что список остается измененным, когда функция, которая использует его, вызывается во второй раз,
  • соблюдая порядок, в котором вывод печатается из источника (который я для вас удобно пронумеровал):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

и запустить его с python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Это нарушает принцип «Наименьшего удивления»?

Этот порядок выполнения часто сбивает с толку новых пользователей Python. Если вы понимаете модель исполнения Python, то она становится вполне ожидаемой.

Обычная инструкция для новых пользователей Python:

Но именно поэтому обычная инструкция для новых пользователей состоит в том, чтобы вместо этого создавать их аргументы по умолчанию:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

При этом используется синглтон None в качестве сторожевого объекта, чтобы сообщить функции, получили ли мы аргумент, отличный от значения по умолчанию. Если мы не получим аргумента, то на самом деле мы хотим использовать новый пустой список, []по умолчанию.

Как сказано в учебном разделе о потоке управления :

Если вы не хотите, чтобы значение по умолчанию было общим для последующих вызовов, вы можете написать такую ​​функцию:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

Самый короткий ответ, вероятно, будет «определение - исполнение», поэтому весь аргумент не имеет строгого смысла. В качестве более надуманного примера вы можете привести это:

def a(): return []

def b(x=a()):
    print x

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

Я согласен с этим, когда вы пытаетесь использовать конструкторы по умолчанию.


20

Простой обходной путь с использованием None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

19

Такое поведение не удивительно, если принять во внимание следующее:

  1. Поведение атрибутов класса только для чтения при попытках присваивания, и это
  2. Функции являются объектами (хорошо объяснено в принятом ответе).

Роль (2) широко освещалась в этой теме. (1) , вероятно, является фактором, вызывающим удивление, так как это поведение не является «интуитивным» при переходе с других языков.

(1) описано в уроке по Python для классов . При попытке присвоить значение атрибуту класса только для чтения:

... все переменные, найденные вне самой внутренней области, доступны только для чтения ( попытка записи в такую ​​переменную просто создаст новую локальную переменную в самой внутренней области, оставив внешнюю переменную с таким же именем без изменений ).

Вернитесь к исходному примеру и рассмотрите вышеупомянутые пункты:

def foo(a=[]):
    a.append(5)
    return a

Здесь fooобъект и aявляется атрибутом foo(доступно в foo.func_defs[0]). Поскольку aэто список, aон изменчив и, следовательно, является атрибутом чтения-записиfoo . Он инициализируется пустым списком, как указано в сигнатуре, когда создается экземпляр функции, и доступен для чтения и записи, пока существует объект функции.

Вызов fooбез переопределения значения по умолчанию использует значение по умолчанию из foo.func_defs. В этом случае foo.func_defs[0]используется для aвнутри области кода объекта функции. Изменения к aизменению foo.func_defs[0], которое является частью fooобъекта и сохраняется между выполнением кода в foo.

Теперь сравните это с примером из документации по эмуляции поведения аргументов по умолчанию в других языках , так что значения сигнатур функции по умолчанию используются каждый раз, когда функция выполняется:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Принимая во внимание (1) и (2) , можно понять, почему это приводит к желаемому поведению:

  • Когда fooобъект функции создается , foo.func_defs[0]устанавливается на Noneнеизменяемый объект.
  • Когда функция выполняется со значениями по умолчанию (без указания параметра для Lвызова функции), foo.func_defs[0]( None) доступно в локальной области как L.
  • При L = []назначении не может быть успешно foo.func_defs[0], потому что этот атрибут только для чтения.
  • Согласно (1) , новая локальная переменная, также названная L, создается в локальной области и используется для оставшейся части вызова функции. foo.func_defs[0]Таким образом, остается неизменным для будущих вызовов foo.

19

Я собираюсь продемонстрировать альтернативную структуру для передачи значения списка по умолчанию в функцию (она одинаково хорошо работает со словарями).

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

Неправильный метод (вероятно ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Вы можете проверить, что это один и тот же объект, используя id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Пер Бретт Слаткин "Эффективный Python: 59 конкретных способов написать лучший Python", пункт 20. Использование Noneи строки документации для указания динамических аргументов по умолчанию (стр. 48)

Соглашение для достижения желаемого результата в Python заключается в предоставлении значения по умолчанию Noneи документировании фактического поведения в строке документации.

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

Предпочитаемый метод :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Для «неправильного метода» могут быть допустимые варианты использования, когда программист намеревался предоставить общий доступ к параметру списка по умолчанию, но это скорее исключение, чем правило.


17

Решения здесь:

  1. Используйте в Noneкачестве значения по умолчанию (или одноразового номера object) и включите его, чтобы создавать значения во время выполнения; или
  2. Используйте в lambdaкачестве параметра по умолчанию и вызывайте его в блоке try, чтобы получить значение по умолчанию (именно для этого предназначена лямбда-абстракция).

Второй вариант хорош, потому что пользователи функции могут передать вызываемый объект, который может уже существовать (например, a type)


16

Когда мы делаем это:

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

... мы относим аргумент aв неназванный списку, если вызывающему абонент не передает значение а.

Чтобы упростить обсуждение, давайте временно дадим неназванному списку имя. Как насчет pavlo?

def foo(a=pavlo):
   ...

В любое время, если вызывающий абонент не сообщает нам, что это aтакое, мы используем его повторно pavlo.

Если он pavloявляется изменяемым (модифицируемым) и в fooконечном итоге модифицирует его, то эффект, который мы заметим в следующий раз, fooвызывается без указания a.

Итак, вот что вы видите (помните, pavloинициализируется как []):

 >>> foo()
 [5]

Сейчас же, pavlo есть [5].

Вызов foo()снова изменяет pavloснова:

>>> foo()
[5, 5]

Указание aпри звонке foo()гарантирует, pavloчто не трогается.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Итак, pavloвсе еще [5, 5].

>>> foo()
[5, 5, 5]

16

Иногда я использую это поведение в качестве альтернативы следующей схеме:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Если singletonиспользуется только use_singletonмне, мне нравится следующий шаблон в качестве замены:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Я использовал это для создания экземпляров клиентских классов, которые обращаются к внешним ресурсам, а также для создания диктовок или списков для запоминания.

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


2
Я предпочитаю добавить декоратор для запоминания и поместить кэш запоминания на сам объект функции.
Стефано Борини

Этот пример не заменяет более сложный шаблон, который вы показываете, потому что вы вызываете _make_singletonво время def в примере аргумента по умолчанию, но во время вызова в глобальном примере. Истинная подстановка будет использовать некое изменяемое поле для значения аргумента по умолчанию, но добавление аргумента дает возможность передавать альтернативные значения.
Янн Вернье

15

Вы можете обойти это, заменив объект (и, следовательно, связать с областью действия):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Уродливо, но это работает.


3
Это хорошее решение в тех случаях, когда вы используете программное обеспечение для автоматического создания документации для документирования типов аргументов, ожидаемых функцией. Если a = None, а затем a [], если a - None, не поможет читателю с первого взгляда понять, что ожидается.
Майкл Скотт Катберт

Классная идея: повторное связывание этого имени гарантирует, что оно никогда не будет изменено. Мне действительно это нравится.
holdenweb

Это именно то, как это сделать. Python не создает копию параметра, поэтому вы должны сделать копию явно. Как только у вас есть копия, вы можете изменить ее, как вам угодно, без каких-либо неожиданных побочных эффектов.
Марк Рэнсом

13

Это может быть правдой, что:

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

полностью соответствует обеим вышеперечисленным функциям, и все же следует подчеркнуть следующее:

  1. Это запутанная особенность, и это неудачно в Python.

Другие ответы или, по крайней мере, некоторые из них либо дают очки 1 и 2, но не 3, либо дают точку 3 и баллы приглушения 1 и 2. Но все три верны.

Возможно, это правда, что переключение лошадей в среднем потоке здесь потребовало бы значительных поломок, и что может быть больше проблем, связанных с изменением Python для интуитивной обработки открывающего фрагмента Стефано. И это может быть правдой, что тот, кто хорошо знал внутренности Python, мог объяснить минное поле последствий. Однако,

Существующее поведение не Pythonic, и Python успешен, потому что очень мало о языке нарушает принцип наименьшего удивления где-либо рядомэто плохо. Это реальная проблема, было бы разумно искоренить это. Это недостаток дизайна. Если вы гораздо лучше понимаете язык, пытаясь отследить поведение, я могу сказать, что C ++ делает все это и даже больше; Вы многому научитесь, перемещаясь, например, по тонким ошибкам указателя. Но это не Pythonic: люди, которые заботятся о Python достаточно, чтобы выдержать это поведение, являются людьми, которых привлекает язык, потому что у Python гораздо меньше сюрпризов, чем у другого языка. Дейблеры и любопытные становятся Pythonistas, когда они удивляются тому, как мало времени требуется, чтобы что-то заработало - не из-за дизайна, я имею в виду скрытую логическую головоломку, которая прорезает интуицию программистов, которые тянутся к Python потому что это просто работает .


6
-1 Хотя это оправданная перспектива, это не ответ, и я с этим не согласен. Слишком много особых исключений порождают их собственные угловые случаи.
Марчин

3
Итак, «удивительно невежественно» говорить, что в Python было бы более целесообразно, чтобы аргумент по умолчанию [] оставался [] при каждом вызове функции?
Христос Хейворд,

3
И это невежественно рассматривать как неудачную идиому, устанавливающую аргумент по умолчанию на None, а затем в теле тела функции, если аргумент == None: аргумент = []? Неужели не принято считать эту идиому несчастной, поскольку часто люди хотят того, чего ожидает наивный новичок, что если вы назначите f (аргумент = []), аргумент автоматически по умолчанию будет иметь значение []?
Христос Хейворд

3
Но в Python частью духа языка является то, что вам не нужно совершать слишком много глубоких погружений; array.sort () работает и работает независимо от того, насколько мало вы понимаете о сортировке, больших значениях и константах. Прелесть Python в механизме сортировки массивов, чтобы привести один из бесчисленных примеров, заключается в том, что вам не нужно глубоко погружаться во внутренности. Иными словами, красота Python заключается в том, что обычно не требуется глубоко погружаться в реализацию, чтобы получить что-то, что просто работает. И есть обходной путь (... если аргумент == Нет: аргумент = []), СБОЙ.
Христос Хейворд

3
В качестве отдельного выражения это x=[]означает «создайте пустой объект списка и привяжите к нему имя« x »». Таким образом, в def f(x=[]), пустой список также создается. Он не всегда привязан к x, поэтому он привязан к суррогату по умолчанию. Позже, когда вызывается f (), значение по умолчанию вынимается и связывается с x. Поскольку это был сам пустой список, который был спрятан, этот же список - единственное, что можно связать с x, независимо от того, было ли внутри что-то застряло или нет. Как может быть иначе?
Джерри Б

10

Это не недостаток дизайна . Любой, кто спотыкается об этом, делает что-то не так.

Я вижу 3 случая, когда вы можете столкнуться с этой проблемой:

  1. Вы намерены изменить аргумент как побочный эффект функции. В этом случае никогда не имеет смысла иметь аргумент по умолчанию. Единственное исключение - когда вы злоупотребляете списком аргументов, чтобы иметь атрибуты функции, напримерcache={} , и вы не должны вызывать функцию с фактическим аргументом вообще.
  2. Вы намерены оставить аргумент без изменений, но вы случайно сделали изменить его. Это ошибка, исправьте это.
  3. Вы намереваетесь изменить аргумент для использования внутри функции, но не ожидали, что модификация будет видимой вне функции. В этом случае вам нужно сделать копию аргумента, был ли он по умолчанию или нет! Python не является языком вызова по значению, поэтому он не делает копию для вас, вам нужно четко об этом сказать.

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


«Делать что-то не так» - это диагноз. Тем не менее, я думаю, что были времена, когда ни один шаблон не был полезен, но, как правило, вы не хотите изменять, если передан изменяемый в этом случае (2). cache={}Картина действительно интервью-единственное решение, в реальном коде вы , вероятно , хотите @lru_cache!
Энди Хейден

9

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

Я дам вам то, что я считаю полезным примером.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

печатает следующее

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Просто измените функцию на:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Я думаю, что ответ на этот вопрос заключается в том, как python передает данные параметру (передача по значению или по ссылке), а не изменчивости или как python обрабатывает оператор «def».

Краткое введение. Во-первых, в Python есть два типа данных: один простой элементарный тип данных, такой как числа, а другой тип данных - объекты. Во-вторых, при передаче данных в параметры python передает элементарный тип данных по значению, т.е. создает локальную копию значения в локальной переменной, но передает объект по ссылке, то есть указатели на объект.

Признавая два вышеизложенных момента, давайте объясним, что случилось с кодом Python. Это происходит только из-за передачи по ссылке для объектов, но не имеет ничего общего с изменяемым / неизменным, или, возможно, с тем фактом, что оператор «def» выполняется только один раз, когда он определен.

[] является объектом, поэтому python передает ссылку на [] a, т. е. aявляется только указателем на [], который находится в памяти как объект. Существует только одна копия [] с множеством ссылок на нее. Для первого foo () список [] изменяется на 1 методом добавления. Но обратите внимание, что существует только одна копия объекта списка, и этот объект теперь становится 1 . При запуске второй функции foo () неверно то, что говорит веб-страница effbot (элементы больше не оцениваются). aоценивается как объект списка, хотя теперь содержимое объекта равно 1 . Это эффект передачи по ссылке! Результат foo (3) может быть легко получен таким же образом.

Для дальнейшей проверки моего ответа давайте рассмотрим два дополнительных кода.

====== № 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]является объектом, так и есть None(первый изменчив, а второй неизменен. Но изменчивость не имеет ничего общего с вопросом). Никого нет где-то в космосе, но мы знаем, что это там, и там есть только одна копия Никого. Таким образом, каждый раз, когда вызывается foo, элементы оцениваются (в отличие от некоторого ответа, что он оценивается только один раз) как None, чтобы быть понятным, ссылка (или адрес) None. Затем в foo элемент изменяется на [], т. Е. Указывает на другой объект с другим адресом.

====== № 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

Вызов foo (1) заставляет элементы указывать на объект списка [] с адресом, скажем, 11111111. Содержимое списка изменяется на 1 в функции foo в дальнейшем, но адрес не изменяется, все же 11111111 Затем приходит foo (2, []). Хотя [] в foo (2, []) имеет то же содержимое, что и параметр по умолчанию [] при вызове foo (1), их адреса разные! Поскольку мы предоставляем параметр явно, itemsон должен взять адрес этого нового [], скажем, 2222222, и вернуть его после внесения некоторых изменений. Теперь foo (3) выполняется. так как толькоxПри условии, элементы должны снова принять значение по умолчанию. Какое значение по умолчанию? Он устанавливается при определении функции foo: объекта списка, расположенного в 11111111. Таким образом, элементы оцениваются как адрес 11111111, имеющий элемент 1. Список, расположенный в 2222222, также содержит один элемент 2, но он не указывается элементами Больше. Следовательно, приложение из 3 составит items[1,3].

Из приведенных выше объяснений видно, что рекомендованная в принятом ответе веб-страница effbot не дала соответствующего ответа на этот вопрос. Более того, я думаю, что точка на веб-странице effbot неверна. Я думаю, что код относительно UI.Button правильный:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

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

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Если мы выполним, x[7]()мы получим 7, как и ожидалось, и x[9]()даст 9, другое значение i.


5
Ваш последний пункт неверен. Попробуйте, и вы увидите, что x[7]()это так 9.
Дункан

2
«python передает элементарный тип данных по значению, т. е. делать локальную копию значения в локальную переменную» совершенно неверно. Я удивлен, что кто-то, очевидно, может очень хорошо знать Python, но при этом иметь такое ужасное непонимание основ. :-(
Veky

6

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


Определение функции влияет на две области: область определения, содержащую функцию, и область выполнения, содержащуюся в функции. Хотя довольно ясно, как блоки отображаются в области видимости, вопрос в том, где def <name>(<args=defaults>)::

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

def nameЧасть должна оценить в определяющей сфере - мы хотим nameбыть доступны там, в конце концов. Оценка функции только внутри себя сделает ее недоступной.

Поскольку parameterэто постоянное имя, мы можем «оценить» его одновременно с def name. Это также имеет то преимущество, что создает функцию с известной сигнатурой name(parameter=...):вместо простой name(...):.

Теперь, когда оценивать default?

Последовательность уже говорит «при определении»: все остальное def <name>(<args=defaults>):лучше всего оценивать и при определении. Задержка его частей была бы удивительным выбором.

Оба варианта также не эквивалентны: если defaultвычисляется во время определения, это все равно может повлиять на время выполнения. Если defaultоценивается во время выполнения, это не может повлиять на время определения. Выбор «при определении» позволяет выразить оба случая, а при выборе «при исполнении» можно выразить только один:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

«Согласованность уже говорит« при определении »: все остальное def <name>(<args=defaults>):лучше всего оценивать и при определении». Я не думаю, что вывод следует из предпосылки. Тот факт, что две вещи находятся в одной строке, не означает, что они должны оцениваться в одной и той же области. defaultэто другая вещь, чем остальная часть строки: это выражение. Оценка выражения - это совсем другой процесс, нежели определение функции.
LarsH

Определения функций @LarsH которые будут оценены в Python. Независимо от того, является ли это оператором ( def) или выражением ( lambda), создание функции означает оценку, особенно ее сигнатуры. И значения по умолчанию являются частью сигнатуры функции. Это не означает, что значения по умолчанию должны быть оценены немедленно - подсказки типа могут не, например. Но это, безусловно, говорит о том, что они должны делать, если нет веских причин не делать этого.
Мистер Мияги

Хорошо, создание функции в некотором смысле означает оценку, но, очевидно, не в том смысле, что каждое выражение в ней оценивается во время определения. Большинство нет. Мне не ясно, в каком смысле сигнатура особенно «оценивается» во время определения, больше, чем тело функции «оценивается» (разбирается в подходящее представление); тогда как выражения в теле функции явно не оцениваются в полном смысле. С этой точки зрения согласованность говорит о том, что выражения в подписи также не должны оцениваться «полностью».
LarsH

Я не имею в виду, что вы не правы, только то, что ваш вывод не следует только из последовательности.
LarsH

@LarsH Значения по умолчанию не являются частью тела, и я не утверждаю, что последовательность является единственным критерием. Можете ли вы сделать предложение, как уточнить ответ?
Мистер Мияги

3

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

Мы «исправим» это поведение с помощью декоратора, который будет копировать значение по умолчанию вместо повторного использования одного и того же экземпляра для каждого позиционного аргумента, оставленного со значением по умолчанию.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Теперь давайте переопределим нашу функцию с помощью этого декоратора:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

Это особенно удобно для функций, которые принимают несколько аргументов. Для сравнения:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

с

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Важно отметить, что указанное выше решение не работает, если вы попытаетесь использовать аргументы ключевых слов, например:

foo(a=[4])

Декоратор может быть настроен, чтобы учесть это, но мы оставляем это как упражнение для читателя;)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.