"Наименьшее удивление" и изменяемый аргумент по умолчанию
Любой, кто возился с 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, не порождая ошибок?)
Редактировать:
Бачек привел интересный пример. Вместе с большинством ваших комментариев и, в частности, с комментариями Utaal, я разработал более подробную информацию:
def a():
print("a executed")
return []
def b(x=a()):
x.append(5)
print(x)
a executed
>>> b()
[5]
>>> b()
[5, 5]
Мне кажется, что дизайнерское решение было относительно того, куда поместить область действия параметров: внутри функции или "вместе" с ней?
Выполнение привязки внутри функции означало бы, что x
она фактически привязана к указанному значению по умолчанию при вызове функции, а не к определенному, что представляло бы глубокий недостаток: def
строка была бы "гибридной" в том смысле, что часть привязки (объекта функции) происходила бы при определении, а часть (присвоение параметров по умолчанию) - во время вызова функции.
Фактическое поведение более последовательное: все в этой строке оценивается при выполнении этой строки, то есть при определении функции.
Переведено автоматически
Ответ 1
На самом деле, это не недостаток дизайна и не из-за внутренних компонентов или производительности. Это просто связано с тем фактом, что функции в Python являются объектами первого класса, а не только фрагментом кода.
Как только вы подумаете об этом таким образом, тогда это полностью обретет смысл: функция - это объект, оцениваемый по своему определению; параметры по умолчанию являются своего рода "данными-членами", и поэтому их состояние может меняться от одного вызова к другому - точно так же, как и у любого другого объекта.
В любом случае, у effbot (Фредрик Ландх) есть очень хорошее объяснение причин такого поведения в значениях параметров по умолчанию в Python. Я нашел это очень понятным, и я действительно рекомендую прочитать это, чтобы лучше понять, как работают функциональные объекты.
Ответ 2
Предположим, у вас есть следующий код
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
Когда я вижу объявление eat , наименее удивительным является мысль, что если первый параметр не задан, то он будет равен кортежу ("apples", "bananas", "loganberries")
Однако, предположим, позже в коде я сделаю что-то вроде
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
тогда, если бы параметры по умолчанию были привязаны при выполнении функции, а не при объявлении функции, я был бы удивлен (в очень плохом смысле), обнаружив, что fruits были изменены. Это было бы более удивительно, 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
, используя значение, идентичное тому, с помощью которого он его ввел, либо человек, который, похоже, не может извлечь свой объект, хотя ключ, который они используют, буквально является тем же объектом, который использовался для его помещения в map (на самом деле, именно поэтому Python не позволяет использовать свои изменяемые встроенные типы данных в качестве ключей словаря).
Ваш пример - хороший пример того, как новички в Python будут удивлены и обижены. Но я бы сказал, что если бы мы "исправили" это, то это только создало бы другую ситуацию, в которой вместо этого они были бы укушены, и эта ситуация была бы еще менее интуитивно понятной. Более того, это всегда имеет место при работе с изменяемыми переменными; вы всегда сталкиваетесь со случаями, когда кто-то может интуитивно ожидать одного или противоположного поведения в зависимости от того, какой код он пишет.
Лично мне нравится текущий подход Python: аргументы функции по умолчанию вычисляются, когда функция определена, и этот объект всегда используется по умолчанию. Я полагаю, что они могли бы использовать пустой список в специальном случае, но такая специальная оболочка вызвала бы еще большее удивление, не говоря уже о обратной несовместимости.
Ответ 3
Соответствующая часть документации:
Значения параметров по умолчанию вычисляются слева направо при выполнении определения функции. Это означает, что выражение вычисляется один раз, когда функция определена, и что одно и то же “предварительно вычисленное” значение используется для каждого вызова. Это особенно важно понимать, когда параметром по умолчанию является изменяемый объект, такой как список или словарь: если функция изменяет объект (например, добавляя элемент в список), значение по умолчанию фактически изменяется. Обычно это не то, что предполагалось. Способ обойти это - использовать
None
в качестве значения по умолчанию и явно протестировать его в теле функции, например:
def whats_on_the_telly(penguin=None):
if penguin is None:
penguin = []
penguin.append("property of the zoo")
return penguin
Ответ 4
Я ничего не знаю о внутренней работе интерпретатора 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):