Вопрос-Ответ

"Least Astonishment" and the Mutable Default Argument

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

Любой, кто возился с 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):
2024-01-31 11:08 python