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

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

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

38 ответов

Решение

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

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

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

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

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

def eat(food=fruits):
    ...

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

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

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

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

AFAICS еще никто не разместил соответствующую часть документации:

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

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

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

a = []

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

Почему a=[] в

def x(a=[]):

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

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

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

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

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

def x(static a=b):

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

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

Я действительно удивлен, что никто не выполнил проницательный самоанализ, предложенный 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__ (позиция [0] в __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. Производительность

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

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

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

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

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

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

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

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

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

  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 изменения - операция выполняется над тем же объектом - и она печатается

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

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

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

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

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

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

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

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

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

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

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

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

def a(): return []

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

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

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

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

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

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

  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,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ужасно, но это работает.

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

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

... мы назначаем аргумент 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]

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

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

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

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

Да, это конструктивный недостаток Python.

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

Значения по умолчанию могли быть разработаны для оценки при вызове функции, а не при ее определении. Вот как это делает Javascript:

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

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

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

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

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

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

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

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

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=None):     # delay default until execution time
    parameter = [] if parameter is None else parameter
    ...

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

Другие вопросы по тегам