python value - «Наименьшее удивление» и параметр Mutable Default Argument




function arguments (25)

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

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


Answers

Простое обходное решение, использующее 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]

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

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) Так называемая проблема «Mutable Default Argument» - это, в общем, специальный пример, демонстрирующий, что:
«Все функции с этой проблемой также страдают от аналогичной проблемы побочного эффекта от фактического параметра ».
Это противоречит правилам функционального программирования, как правило, неоправданно и должны быть установлены вместе.

Пример:

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

Пример проблемы для аналогичного вопроса 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Является частным атрибутом)

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

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

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


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

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

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


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

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

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


Верно, что:

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

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

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

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

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

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


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

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

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

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

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

Посмотрите на оригинальный пример и рассмотрите приведенные выше пункты:

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

Вот fooобъект и aявляется атрибутом foo(доступно в foo.func_defs[0]). Поскольку aэто список, он aявляется изменяемым и, таким образом, является атрибутом read-write 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], потому что этот атрибут доступен только для чтения.
  • Per (1) , новая локальная переменная, также именованная L, создается в локальной области и используется для остальной части вызова функции. foo.func_defs[0]таким образом, остается неизменным для будущих вызовов foo.

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

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


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

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

Неверный метод (возможно ...) :

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

Per Brett Slatkin's «Эффективный Python: 59 конкретных способов записи лучшего Python», пункт 20: Использование Noneи Docstrings для указания динамических аргументов по умолчанию (стр. 48)

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

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

Предпочтительный метод :

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]

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


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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

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

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

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

[] - это объект, поэтому python передает ссылку [] на a, т. е. aявляется только указателем на [], который лежит в памяти как объект. Существует только одна копия [] с, однако, многими ссылками на нее. Для первого foo () список [] изменен на 1 методом append. Но учтите, что существует только одна копия объекта списка, и этот объект теперь становится 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, но он не указывается элементами any Больше. Следовательно, добавление 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.


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

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

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


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

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

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

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

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

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

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

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


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

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]

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

def a(): return []

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

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

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


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

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

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


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

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), который мы будем игнорировать.

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

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


Если бы у меня были мои детекторы, постинкремент и многие операторы последовательности точек были бы разделены на две или три части; в случае постинкремента это утверждение типа «a = (b ++ + c ++);» будет эффективно переведено как "a = postinc1 (b) + postinc1 (c); postinc2 (b); postinc2 (c);"; вторая часть постинкремента будет пустой функцией. В реальной реализации вызовы postinc2 () часто должны происходить, в то время как другие результаты находятся в стеке оценки; это не должно быть слишком сложно для реализации компилятором.

В случае «&&» или «||» первая часть оператора будет работать только с левым операндом; если он вернул ненулевое (для &&) или ненулевое (для ||), то вторая часть будет работать с обоими операндами.

В случае «?» / «:» Первая часть оператора будет работать только с первым операндом. Если он вернул ненулевое значение, вторая часть будет работать с первым и вторым параметрами; в противном случае третья часть будет работать с первым и третьим параметрами.

Какие-нибудь языки делают что-нибудь подобное? Кажется странным, что C ++ позволяет переопределять операторы способами, которые нарушают поведение последовательности, но не позволяет их переопределять способами, которые его сохраняют.





python language-design least-astonishment