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

Любой, кто возился с 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линия будет «гибридной» в том смысле, что часть привязки (из объект функции) будет происходить при определении, а часть (назначение параметров по умолчанию) во время вызова функции.

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

15.07.2009 18:00:37
Jonathan 6.02.2012 20:54:07
Я не сомневаюсь, что изменчивые аргументы нарушают принцип наименьшего удивления для обычного человека, и я видел, как новички зашли туда, а затем героически заменили списки рассылки на почтовые кортежи. Тем не менее изменчивые аргументы все еще соответствуют Python Zen (Pep 20) и попадают в предложение «очевидное для голландского» (понимаемое / используемое программистами Python). Рекомендуемый обходной путь со строкой документа является лучшим, однако устойчивость к строкам документа и любым (написанным) документам в наши дни не так уж редка. Лично я бы предпочел декоратор (скажем, @fixed_defaults).
Serge 6.04.2017 16:04:44
Мой аргумент, когда я сталкиваюсь с этим: «Зачем вам нужно создавать функцию, которая возвращает изменяемый файл, который может быть изменяемым, который вы могли бы передать функции? Либо он изменяет изменяемый, либо создает новый. Зачем вам это нужно? сделать оба с одной функцией? И почему интерпретатор должен быть переписан, чтобы позволить вам сделать это без добавления трех строк в ваш код? " Потому что здесь мы говорим о переписывании способа, которым интерпретатор обрабатывает определения функций и их вызовы. Это очень много для крайне необходимого варианта использования.
Alan Leuthard 1.06.2017 21:22:20
Msgstr "Новички в Python ожидают, что эта функция будет всегда возвращать список только с одним элементом:. [5]" Я новичок в Python, и я не ожидал бы этого, потому что, очевидно foo([1]), вернется [1, 5], а не [5]. Что вы хотели сказать, так это то, что новичок ожидал, что функция, вызываемая без параметров , всегда будет возвращаться [5].
symplectomorphic 6.07.2017 16:08:22
Этот вопрос задает вопрос: «Почему этот [неправильный путь] был реализован так?» Это не спрашивает, "Каков правильный путь?" , который описан в [ Почему использование arg = None устраняет проблему изменяемого аргумента Python по умолчанию? ] * ( stackoverflow.com/questions/10676729/… ). Новые пользователи почти всегда менее заинтересованы в первом и гораздо больше во втором, так что иногда это очень полезная ссылка / обман.
smci 21.04.2019 08:48:07
30 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

1604
17.07.2009 21:29:39
Всем, кто читает вышеуказанный ответ, я настоятельно рекомендую вам потратить время на прочтение связанной статьи Effbot. Как и вся другая полезная информация, очень полезна информация о том, как использовать эту языковую функцию для кэширования / запоминания результатов!
Cam Jackson 14.10.2011 00:05:52
Даже если это объект первого класса, можно по-прежнему представлять проект, в котором код для каждого значения по умолчанию сохраняется вместе с объектом и переоценивается при каждом вызове функции. Я не говорю, что это было бы лучше, просто функции, являющиеся первоклассными объектами, не исключают этого полностью.
gerrit 11.01.2013 10:55:25
Извините, но все, что считается "самым большим WTF в Python", определенно является недостатком дизайна . Это является источником ошибок для всех в какой-то момент, потому что никто не ожидает такого поведения вначале - что означает, что его не следовало разрабатывать таким образом с самого начала. Мне все равно, через какие скачки они должны были прыгать, они должны были спроектировать Python так, чтобы аргументы по умолчанию были нестатичными.
BlueRaja - Danny Pflughoeft 7.06.2013 21:28:27
Независимо от того, является ли это недостатком проекта, ваш ответ, кажется, подразумевает, что это поведение каким-то образом необходимо, естественным и очевидным, учитывая, что функции являются первоклассными объектами, а это просто не тот случай. Python имеет замыкания. Если вы замените аргумент по умолчанию присваиванием в первой строке функции, он будет оценивать выражение при каждом вызове (возможно, с использованием имен, объявленных в рамках объема). Нет никакой причины, что было бы невозможно или разумно оценивать аргументы по умолчанию каждый раз, когда функция вызывается точно таким же образом.
Mark Amery 8.01.2014 22:16:13
Дизайн не следует непосредственно из functions are objects. В вашей парадигме предложение состоит в том, чтобы реализовать значения функций по умолчанию в качестве свойств, а не атрибутов.
bukzor 3.05.2014 20:46:09

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

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: аргументы функции по умолчанию оцениваются, когда функция определена, и этот объект всегда является значением по умолчанию. Я полагаю, что они могли бы использовать особый случай, используя пустой список, но такой особый случай может вызвать еще большее удивление, не говоря уже о несовместимости в обратном направлении.

272
27.11.2016 14:24:50
Я думаю, что это вопрос дебатов. Вы действуете по глобальной переменной. Любая оценка, выполненная где-либо в вашем коде с использованием вашей глобальной переменной, теперь будет (правильно) называться («черника», «манго»). параметр по умолчанию может быть как любой другой случай.
Stefano Borini 15.07.2009 18:16:08
На самом деле, я не думаю, что согласен с вашим первым примером. Я не уверен, что мне нравится идея изменить инициализатор подобным образом, но если бы я это сделал, я бы ожидал, что он будет вести себя точно так, как вы описываете - изменение значения по умолчанию на ("blueberries", "mangos").
Ben Blank 15.07.2009 18:26:45
Параметр по умолчанию является , как и любой другой случай. Что неожиданно, так это то, что параметр является глобальной переменной, а не локальной. Что, в свою очередь, объясняется тем, что код выполняется при определении функции, а не при вызове. Как только вы это получите, и то же самое относится и к занятиям, это совершенно ясно.
Lennart Regebro 15.07.2009 18:59:01
Я нахожу пример вводящим в заблуждение, а не блестящим. Если some_random_function()дописывает fruitsвместо присвоения ему поведение eat() будет меняться. Так много для нынешнего замечательного дизайна. Если вы используете аргумент по умолчанию, на который ссылаются в другом месте, а затем изменяете ссылку извне функции, вы напрашиваетесь на неприятности. Настоящий WTF - это когда люди определяют свежий аргумент по умолчанию (литерал списка или вызов конструктора) и все еще получают бит.
alexis 9.10.2014 15:37:29
Вы просто явно объявили globalи переназначили кортеж - нет ничего удивительного, если eatпосле этого все будет работать иначе.
user3467349 26.01.2015 16:07:31

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

83
19.12.2014 22:53:35
Как вы определяете атрибут класса, который отличается для каждого экземпляра класса?
Kieveli 15.07.2009 19:04:39
Если это отличается для каждого экземпляра, это не атрибут класса. Атрибуты класса являются атрибутами класса. Отсюда и название. Следовательно, они одинаковы для всех случаев.
Lennart Regebro 15.07.2009 19:17:21
Как вы определяете атрибут в классе, который отличается для каждого экземпляра класса? (Переопределено для тех, кто не может определить, что человек, не знакомый с соглашениями об именах Python, может задавать вопрос о нормальных переменных-членах класса).
Kieveli 16.07.2009 14:03:46
@Kievieli: Вы говорите о нормальных переменных-членах класса. :-) Вы определяете атрибуты экземпляра, говоря self.attribute = value в любом методе. Например, __init __ ().
Lennart Regebro 16.07.2009 14:14:18
@Kieveli: Два ответа: вы не можете, потому что любая вещь, которую вы определяете на уровне класса, будет атрибутом класса, а любой экземпляр, который обращается к этому атрибуту, будет иметь доступ к одному и тому же атрибуту класса; Вы можете / sort of /, используя propertys - которые на самом деле являются функциями уровня класса, которые действуют как обычные атрибуты, но сохраняют атрибут в экземпляре вместо класса (используя, self.attribute = valueкак сказал Леннарт).
Ethan Furman 7.01.2012 04:45:56

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

  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 изменения - операция выполняется над тем же объектом - и она печатается
51
24.10.2017 06:34:34
(На самом деле, add - плохой пример, но целочисленность неизменяемости по-прежнему является моей главной точкой.)
Anon 15.07.2009 23:54:43
Осознал это к моему огорчению после проверки того, что с b, установленным в [], b .__ add __ ([1]) возвращает [1], но также оставляет b все еще [], даже если списки изменчивы. Виноват.
Anon 16.07.2009 00:03:30
@ ANon: есть __iadd__, но он не работает с int. Конечно. :-)
Veky 8.05.2014 13:16:31

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

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), который мы проигнорируем.

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

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

33
15.07.2009 20:18:14
Это не должно быть закрытием - лучший способ думать об этом - просто заставить байт-код создавать значения по умолчанию в первой строке кода - после того, как вы все равно скомпилируете тело в этот момент - между реальным кодом нет никакой разницы в аргументах и ​​код в теле.
Brian 16.07.2009 09:39:20
Да, но это все равно замедлит Python, и это будет довольно удивительно, если вы не сделаете то же самое для определений классов, что сделает его тупо медленным, поскольку вам придется перезапускать полное определение класса каждый раз, когда вы создаете экземпляр учебный класс. Как уже упоминалось, исправление будет более удивительным, чем проблема.
Lennart Regebro 16.07.2009 11:49:40
Договорились с Леннартом. Как любит говорить Гвидо, для каждой языковой функции или стандартной библиотеки есть кто-то , кто ее использует.
Jason Baker 16.07.2009 13:21:22
Изменить его сейчас было бы безумием - мы просто исследуем, почему это так. Если бы для начала была проведена поздняя оценка по умолчанию, это не обязательно было бы удивительно. Совершенно верно, что такое основное различие в синтаксическом анализе имело бы радикальные и, вероятно, многие неясные последствия для языка в целом.
Glenn Maynard 16.07.2009 18:10:05

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

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 у вас нет этой выгоды.

25
2.04.2013 21:52:30

Я ничего не знаю о внутренней работе интерпретатора 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):
116
9.05.2019 09:15:36
Вы можете сделать: def x (a = None): И затем, если a None, установите a = datetime.datetime.now ()
Anon 16.07.2009 00:18:29
Спасибо тебе за это. Я не мог понять, почему это бесит меня. Вы сделали это красиво с минимумом нечеткости и путаницы. Как кто-то, пришедший из системного программирования на C ++ и иногда наивно «переводящий» языковые функции, этот ложный друг пнул меня в тупик, как атрибуты класса. Я понимаю, почему все так, но я не могу не любить это, независимо от того, что из этого может получиться положительного. По крайней мере, это настолько противоречит моему опыту, что я, вероятно (надеюсь), никогда этого не забуду ...
AndreasT 22.04.2011 09:33:25
@ Андреас, как только вы используете Python достаточно долго, вы начинаете понимать, насколько логично для Python интерпретировать вещи как атрибуты класса так, как он это делает - это только из-за специфических особенностей и ограничений языков, таких как C ++ (и Java, и C # ...) что имеет смысл class {}интерпретировать содержимое блока как принадлежащее экземплярам :) Но когда классы являются первоклассными объектами, очевидно, что их содержимое (в памяти) естественно отражает их содержимое (в коде).
Karl Knechtel 22.07.2011 19:55:07
Нормативная структура не является причудой или ограничением в моей книге. Я знаю, что это может быть неуклюже и безобразно, но вы можете назвать это «определением» чего-либо. Динамические языки кажутся мне немного похожими на анархистов: конечно, все свободны, но вам нужна структура, чтобы кто-то мог очистить мусор и проложить дорогу. Думаю, я старый ... :)
AndreasT 26.07.2011 08:54:28
Функция определения выполняется в модуле времени загрузки. Тело функции выполняется во время вызова функции. Аргумент по умолчанию является частью определения функции, а не тела функции. (Это становится более сложным для вложенных функций.)
Lutz Prechelt 30.03.2015 11:28:13

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

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=[]

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

57
16.07.2009 19:13:35
Вы также можете достичь самоанализа, если для каждого есть функция для создания аргумента по умолчанию вместо значения. модуль проверки просто вызовет эту функцию.
yairchu 16.07.2009 10:24:57
@SilentGhost: я говорю о том, было ли изменено поведение, чтобы воссоздать его - создание его один раз является текущим поведением, и почему существует изменяемая проблема по умолчанию.
Brian 16.07.2009 10:59:05
@yairchu: Это предполагает, что конструкция безопасна для этого (т.е. не имеет побочных эффектов). Самоанализ аргументов не должен ничего делать , но оценка произвольного кода вполне может иметь эффект.
Brian 16.07.2009 11:02:31
Различный языковой дизайн часто просто означает писать вещи по-другому. Ваш первый пример может быть легко написан как: _exорого = дорогой (); def foo (arg = _exорого), если вы специально не хотите его переоценивать.
Glenn Maynard 16.07.2009 18:23:28
@Glenn - это то, что я имел в виду под «кэшированием переменной извне» - это немного более многословно, и в результате вы получите дополнительные переменные в своем пространстве имен.
Brian 16.07.2009 19:04:51

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

def a(): return []

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

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

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

23
20.05.2018 23:22:19

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

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

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

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

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

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

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

13
16.07.2009 19:17:59
-1 Хотя это оправданная перспектива, это не ответ, и я с этим не согласен. Слишком много особых исключений порождают их собственные угловые случаи.
Marcin 7.07.2012 19:24:33
Итак, «удивительно невежественно» говорить, что в Python было бы более целесообразно, чтобы аргумент по умолчанию [] оставался [] каждый раз, когда вызывается функция?
Christos Hayward 27.12.2012 22:09:35
И это невежественно рассматривать как неудачную идиому, устанавливающую аргумент по умолчанию на None, а затем в теле тела установки функции, если аргумент == Нет: аргумент = []? Неужели не принято считать эту идиому неудачной, поскольку часто люди хотят того, чего ожидает наивный новичок, что если вы назначите f (аргумент = []), аргумент автоматически по умолчанию будет иметь значение []?
Christos Hayward 27.12.2012 22:11:58
Но в Python частью духа языка является то, что вам не нужно совершать слишком много глубоких погружений; array.sort () работает и работает независимо от того, насколько мало вы понимаете о сортировке, больших значениях и константах. Прелесть Python в механизме сортировки массивов, чтобы привести один из бесчисленных примеров, состоит в том, что вам не нужно глубоко погружаться во внутренности. Иными словами, красота Python заключается в том, что обычно не требуется глубоко погружаться в реализацию, чтобы получить что-то, что просто работает. И есть обходной путь (... если аргумент == Нет: аргумент = []), СБОЙ.
Christos Hayward 27.12.2012 22:41:56
Как автономное выражение x=[]означает «создать пустой объект списка и привязать к нему имя« x »». Таким образом, в def f(x=[]), пустой список также создается. Он не всегда привязан к х, поэтому он привязан к суррогату по умолчанию. Позже, когда вызывается f (), значение по умолчанию вынимается и связывается с x. Поскольку это был сам пустой список, который был сжат, этот же список - единственное, что можно связать с x, независимо от того, было ли внутри что-то застряло или нет. Как может быть иначе?
Jerry B 5.10.2013 06:18:32

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

>>> 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 или нет, и есть ли задействованные аргументы по умолчанию или нет.

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

29
23.05.2011 04:24:30
Хотя это связано, я думаю, что это отличное поведение (как мы ожидаем, appendчтобы изменить a"на месте"). То, что изменяемый файл по умолчанию не восстанавливается при каждом вызове, является "неожиданным" битом ... по крайней мере для меня. :)
Andy Hayden 24.08.2012 12:27:38
@AndyHayden, если ожидается, что функция будет изменять аргумент, зачем иметь значение по умолчанию?
Mark Ransom 17.10.2017 13:31:05
@MarkRansom единственный пример, который я могу придумать cache={}. Тем не менее, я подозреваю, что возникает «наименьшее удивление», когда вы не ожидаете (или не хотите) функцию, которую вы вызываете, чтобы изменить аргумент.
Andy Hayden 17.10.2017 17:56:43
@AndyHayden Я оставил свой собственный ответ здесь с расширением этого чувства. Дайте мне знать, что вы думаете. Я мог бы добавить ваш пример cache={}в это для полноты.
Mark Ransom 17.10.2017 18:02:52
@AndyHayden Суть моего ответа в том, что если вы когда-нибудь удивитесь, случайно изменив значение аргумента по умолчанию, то у вас есть еще одна ошибка, заключающаяся в том, что ваш код может случайно изменить значение вызывающего, когда значение по умолчанию не использовалось. И обратите внимание, что использование Noneи присвоение реального значения по умолчанию, если аргумент arg None , не решает эту проблему (по этой причине я считаю это анти-паттерном). Если вы исправите другую ошибку, избегая изменяющихся значений аргументов, независимо от того, имеют ли они значения по умолчанию, тогда вы никогда не заметите и не позаботитесь об этом «удивительном» поведении.
Ben 17.10.2017 21:44:29

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

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

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

17
30.06.2013 16:20:35

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

  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
1.04.2013 15:30:56

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

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

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
239
30.01.2020 18:00:14
Фразы «это вообще не то, что было задумано», а «способ обойти это» пахнут, как будто они документируют недостаток дизайна.
bukzor 3.05.2014 20:53:15
@ Мэтью: я хорошо знаю, но это не стоит ловушки. Как правило, по этой причине вы, как правило, видите, что руководства по стилю и линтеры помечают изменяемые значения по умолчанию как неправильные Явный способ сделать то же самое - вставить атрибут в функцию ( function.data = []) или, что еще лучше, создать объект.
bukzor 19.06.2014 03:59:30
@bukzor: Подводные камни должны быть отмечены и задокументированы, поэтому этот вопрос хорош и получил столько голосов. В то же время, ловушки не обязательно должны быть удалены. Сколько новичков в Python передало список функции, которая его изменила, и были шокированы, увидев, что изменения обнаружены в исходной переменной? Тем не менее, изменчивые типы объектов замечательны, когда вы понимаете, как их использовать. Я предполагаю, что это просто сводится к мнению об этой конкретной ловушке.
Matthew 19.06.2014 17:54:06
Фраза «это вообще не то, что было задумано» означает «не то, что на самом деле хотел сделать программист», а не «не то, что должен делать Python».
holdenweb 19.12.2014 11:48:14
@holdenweb Ого, я опоздал на вечеринку. Учитывая контекст, букзор совершенно прав: они документируют поведение / последствия, которые не были «предназначены», когда они решили, что язык должен выполнить определение функции. Поскольку это непреднамеренное последствие их выбора дизайна, это недостаток дизайна. Если бы это не было недостатком дизайна, не было бы даже необходимости предлагать «способ обойти это».
code_dredd 3.10.2017 07:35:53

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) В некоторых случаях изменчивое поведение параметров по умолчанию полезно .

33
23.05.2017 11:47:32
Надеюсь, вы знаете, что Python не является функциональным языком программирования.
Veky 8.05.2014 13:18:07
Да, Python - это мультипарагигмальный язык с некоторыми функциональными возможностями. («Не делайте каждую проблему похожей на гвоздь только потому, что у вас есть молоток».) Многие из них соответствуют лучшим практикам Python. У Python есть интересное HOWTO Функциональное программирование. Другие функции - это замыкания и каррирование, которые здесь не упоминаются.
hynekcer 8.05.2014 15:54:34
На этом позднем этапе я бы также добавил, что семантика присваивания Python была разработана специально, чтобы избежать копирования данных там, где это необходимо, поэтому создание копий (и особенно глубоких копий) отрицательно повлияет как на время выполнения, так и на использование памяти. Поэтому их следует использовать только в случае необходимости, но новичкам часто трудно понять, когда это так.
holdenweb 16.01.2018 14:27:07
@holdenweb Я согласен. Временная копия - это самый обычный способ, а иногда и единственно возможный способ защиты исходных изменяемых данных от посторонней функции, которая потенциально изменяет их. К счастью, функция, которая необоснованно изменяет данные, считается ошибкой и, следовательно, необычной.
hynekcer 17.01.2018 21:41:33
Я согласен с этим ответом. И я не понимаю, почему def f( a = None )конструкция рекомендуется, когда вы действительно имеете в виду что-то еще. Копирование - это нормально, потому что вы не должны изменять аргументы. И когда вы это делаете if a is None: a = [1, 2, 3], вы все равно копируете список.
koddo 16.02.2018 16:15:52

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

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

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

15
15.01.2013 11:02:03
Это хорошее решение в тех случаях, когда вы используете программное обеспечение для автоматического создания документации для документирования типов аргументов, ожидаемых функцией. Если a = None, а затем a [], если a - None, не поможет читателю с первого взгляда понять, что ожидается.
Michael Scott Cuthbert 20.01.2013 06:55:05
Классная идея: повторное связывание этого имени гарантирует, что оно никогда не будет изменено. Мне действительно это нравится.
holdenweb 16.01.2018 14:29:04
Это именно то, как это сделать. Python не создает копию параметра, поэтому вы должны сделать копию явно. Если у вас есть копия, вы можете изменить ее, как вам угодно, без каких-либо неожиданных побочных эффектов.
Mark Ransom 25.05.2018 16:56:48

Простой обходной путь с использованием 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
28.02.2013 11:10:16

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

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

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
9
23.07.2013 10:07:58

Я думаю, что ответ на этот вопрос заключается в том, как 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.

7
22.08.2013 05:58:41
Ваш последний пункт неверен. Попробуйте, и вы увидите, что x[7]()это так 9.
Duncan 2.10.2013 13:29:23
«python передает элементарный тип данных по значению, то есть делать локальную копию значения в локальную переменную» совершенно неверно. Я удивлен, что кто-то, очевидно, может очень хорошо знать Python, но при этом иметь такое ужасное непонимание основ. :-(
Veky 19.11.2014 09:07:54

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

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
11.09.2014 22:05:43

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

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()

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

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

16
6.02.2015 12:56:53
Я предпочитаю добавить декоратор для запоминания и поместить кэш запоминания на сам объект функции.
Stefano Borini 6.02.2015 07:34:32
Этот пример не заменяет более сложный шаблон, который вы показываете, потому что вы вызываете _make_singletonво время def в примере аргумента по умолчанию, но во время вызова в глобальном примере. Истинная подстановка будет использовать некое изменяемое поле для значения аргумента по умолчанию, но добавление аргумента дает возможность передавать альтернативные значения.
Yann Vernier 19.11.2017 08:29:15

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

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
26
26.03.2015 23:14:01
на самом деле это может быть немного запутанным для новичков, так как a = a + [1]перегрузки a... рассмотрите возможность изменить его b = a + [1] ; print id(b)и добавить строку a.append(2). Это сделает более очевидным, что +в двух списках всегда создается новый список (назначенный b), в то время как измененный aможет иметь то же самое id(a).
Jörn Hees 8.04.2017 13:47:17

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

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

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

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

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

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

54
26.08.2016 08:33:17
Мне потребовался год, чтобы обнаружить, что такое поведение портит мой код на производстве, и в итоге я удалил полную функцию, пока я случайно не наткнулся на этот недостаток дизайна. Я использую Django. Поскольку в промежуточной среде не было много запросов, эта ошибка никогда не влияла на QA. Когда мы начали работу и получили много одновременных запросов - некоторые служебные функции начали перезаписывать параметры друг друга! Делаем дыры в безопасности, ошибки, а что нет.
oriadam 5.09.2015 13:09:46
@oriadam, без обид, но мне интересно, как ты выучил Python, не сталкиваясь с этим раньше. Я только сейчас изучаю Python, и эта возможная ловушка упоминается в официальном руководстве по Python вместе с первым упоминанием аргументов по умолчанию. (Как упомянуто в пункте 4 этого ответа.) Я полагаю, что мораль - довольно не сочувственно - читать официальные документы языка, который вы используете для создания производственного программного обеспечения.
Wildcard 30.08.2016 02:26:54
Кроме того, было бы удивительно (для меня), если бы функция неизвестной сложности была вызвана в дополнение к вызову функции, который я делаю.
Vatine 2.09.2016 13:26:15

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

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
8
6.09.2018 21:29:08

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

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

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

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]

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

18
12.09.2015 20:41:53

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

Я действительно удивлен, что никто не выполнил проницательную интроспекцию, предложенную 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.

63
11.10.2018 16:33:49
Требуется ли id(...)для этой последней проверки, или isоператор ответил бы на тот же вопрос?
das-g 9.03.2016 08:09:30
С @ das-g все isбудет в порядке, я просто использовал, id(val)потому что думаю, что он может быть более интуитивным.
Dimitris Fasarakis Hilliard 9.03.2016 08:20:27
Использование Noneпо умолчанию серьезно ограничивает полезность __defaults__самоанализа, поэтому я не думаю, что это работает как защита от __defaults__работы так, как работает. Ленивая оценка сделала бы больше, чтобы сохранить функциональные значения по умолчанию полезными с обеих сторон.
Brilliand 18.10.2019 05:32:26

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
23
23.12.2017 21:18:35

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

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

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

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

10
17.10.2017 18:04:59
«Делать что-то не так» - это диагноз. Тем не менее, я думаю, что были времена, когда ни один шаблон не был полезен, но, как правило, вы не хотите изменять, если передан изменяемый в этом случае (2). cache={}Картина действительно интервью-единственное решение, в реальном коде вы , вероятно , хотите @lru_cache!
Andy Hayden 17.10.2017 18:19:46

Архитектура

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

def a(b=[]):
    pass

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

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

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

Если мы решим эту проблему с помощью полиморфизма, мы расширим список python или поместим его в класс, а затем выполним над ним свою функцию.

Но подожди, ты скажешь, мне нравятся мои строки.

Ну, угадай что. Код - это больше, чем просто способ управления поведением оборудования. Это способ:

  • общение с другими разработчиками, работающими над тем же кодом.

  • возможность изменять поведение оборудования при возникновении новых требований.

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

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

Разделив эту функцию на две функции, нам нужен класс

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

Выполняется по

a = ListNeedsFives()
a.foo()
a.b

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

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

Почему бы не сделать это?

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

Конструктор класса является общепризнанным компонентом для всех, кто занимался объектно-ориентированным программированием. Размещение логики, которая обрабатывает создание экземпляров списка в конструкторе, делает когнитивную нагрузку понимания того, что делает код, меньше.

Метод foo()не возвращает список, почему бы и нет?

Возвращая автономный список, вы можете предположить, что безопасно делать то, что вам нравится. Но это может быть не так, поскольку объект также является общим a. Принуждение пользователя ссылаться на него как a.bнапоминает ему, где список принадлежит. Любой новый код, который нужно изменить a.b, естественно будет помещен в класс, которому он принадлежит.

def dontdothis(b=None):функция подписи не имеет ни одного из этих преимуществ.

-4
10.11.2017 15:43:47
Я думаю, что ваш комментарий «Код - это больше, чем просто способ управления поведением аппаратного обеспечения» - это замечательно, но просто сказать, что аргументы по умолчанию являются запахом кода, кажется слишком драматичным. Есть совершенно веские причины для аргументов по умолчанию.
Bryan Oakley 14.11.2017 01:29:06
@BryanOakley Ну ... я честно написал этот ответ, когда у меня было немного мрачное настроение ... подумал, какого чёрта, я могу просто наплевать на мое мнение. В любом случае, этот вопрос довольно самоуверенный ... Так что ... да, спасибо за похвалу, хотя я принимаю вину также близко к сердцу
firelynx 14.11.2017 19:05:05

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
    ...
6
8.08.2019 07:39:43
«Согласованность уже говорит« при определении »: все остальное def <name>(<args=defaults>):лучше всего оценивать и при определении». Я не думаю, что вывод следует из предпосылки. То, что две вещи находятся в одной строке, не означает, что они должны оцениваться в одной и той же области. defaultэто другая вещь, чем остальная часть строки: это выражение. Оценка выражения - это совсем другой процесс, нежели определение функции.
LarsH 23.09.2019 14:38:21
Определения функций @LarsH которые будут оценены в Python. Независимо от того, является ли это оператором ( def) или выражением ( lambda), создание функции означает оценку, особенно ее сигнатуры. И значения по умолчанию являются частью подписи функции. Это не означает, что значения по умолчанию должны оцениваться немедленно - подсказки типа могут, например, нет. Но это, безусловно, говорит о том, что они должны делать, если нет веских причин не делать этого.
MisterMiyagi 23.09.2019 15:22:41
Хорошо, создание функции в некотором смысле означает оценку, но, очевидно, не в том смысле, что каждое выражение в ней оценивается во время определения. Большинство нет. Мне не ясно, в каком смысле сигнатура особенно «оценивается» во время определения, больше, чем тело функции «оценивается» (разбирается в подходящее представление); тогда как выражения в теле функции явно не оцениваются в полном смысле. С этой точки зрения согласованность говорит о том, что выражения в подписи также не должны оцениваться «полностью».
LarsH 24.09.2019 15:09:18
Я не имею в виду, что вы не правы, только то, что ваш вывод не следует только из последовательности.
LarsH 24.09.2019 15:09:22
@LarsH Значения по умолчанию не являются частью тела, и я не утверждаю, что последовательность является единственным критерием. Можете ли вы сделать предложение, как уточнить ответ?
MisterMiyagi 24.09.2019 15:15:33