Разница между мутацией, повторным связыванием, копированием значения и оператором присваивания - PullRequest
9 голосов
/ 31 января 2012
#!/usr/bin/env python3.2

def f1(a, l=[]):
    l.append(a)
    return(l)

print(f1(1))
print(f1(1))
print(f1(1))

def f2(a, b=1):
    b = b + 1
    return(a+b)

print(f2(1))
print(f2(1))
print(f2(1))

В f1 аргумент l имеет присвоение значения по умолчанию и оценивается только один раз, поэтому три print выводят 1, 2 и 3. Почему f2 не делаетпохожее?

Вывод:

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

  • Я нашел этот хороший учебник по теме.

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

Ответы [ 4 ]

5 голосов
/ 31 января 2012

Это подробно описано в относительно популярном вопросе SO , но я постараюсь объяснить проблему в вашем конкретном контексте.


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

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

Рассмотрите альтернативную функцию:

def f3(a, l= []):
   l = l + [a]
   return l

Это ведет себя как f2 и не продолжает добавляться в список по умолчанию.Это связано с тем, что он создает новый l, не изменяя объект в параметре по умолчанию.


Общий стиль в python - назначить параметр по умолчанию None, а затем назначить новый список.,Это обходит всю эту двусмысленность.

def f1(a, l = None):
   if l is None:
       l = []

   l.append(a)

   return l
5 голосов
/ 31 января 2012

Поскольку в f2 имя b является отскоком, тогда как в f1 объект l мутирует.

3 голосов
/ 31 января 2012

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

Имена в Python - это такие вещи, как a, f1, b. Они существуют только в определенных областях (т. Е. Вы не можете использовать b вне функции, которая его использует). Во время выполнения имя ссылается на значение , но в любой момент может быть восстановлено до нового значения с помощью операторов присваивания, таких как:

a = 5
b = a
a = 7

Значения создаются в некоторой точке вашей программы, и на них можно ссылаться по именам, но также по слотам в списках или других структурах данных. В приведенном выше примере имя a привязано к значению 5, а затем - к значению 7. Это не влияет на значение 5, которое всегда равно значению 5, независимо от количества имен. в настоящее время связаны с этим.

С другой стороны, присвоение b делает привязку имени b к значению , на которое ссылается a в тот момент времени. Повторная привязка имени a впоследствии не влияет на значение 5 и, следовательно, не влияет на имя b, которое также связано со значением 5.

Назначение всегда работает в Python таким образом. Это никогда никак не влияет на значения. (За исключением того, что некоторые объекты содержат «имена»; повторное связывание этих имен, очевидно, влияет на объект, содержащий имя, но не влияет на значения, имя которых упоминалось до или после изменения)

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


Теперь мы можем видеть, что происходит в вашем примере.

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

def f1(a, l=[]):
    l.append(a)
    return(l)

l - это не что-нибудь, потому что l - это только имя в рамках функции f1, и мы не внутри этой функции. Однако значение [] хранится где-то в другом месте.

Когда выполнение Python переходит в вызов в f1, он связывает все имена аргументов (a и l) с соответствующими значениями - либо значения, переданные вызывающей стороной, либо значения по умолчанию, созданные при определении функции. Поэтому, когда существа Python выполняют вызов f3(5), имя a будет связано со значением 5, а имя l будет связано с нашим списком по умолчанию.

Когда Python выполняет l.append(a), назначения не видно, поэтому мы ссылаемся на текущие значения l и a. Таким образом, если это как-то повлияет на l, оно может сделать это только путем изменения значения, к которому относится l, и это действительно так. Метод append списка изменяет список, добавляя элемент в конец. Таким образом, после этого нашего значения списка, , которое по-прежнему является тем же значением, сохраненным в качестве аргумента по умолчанию f1, теперь к нему добавлено 5 (текущее значение a), и оно выглядит как [5].

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

Теперь рассмотрим f2:

def f2(a, b=1):
    b = b + 1
    return(a+b)

Здесь, как и прежде, значение 1 спрятано где-то, чтобы служить значением по умолчанию для b, и когда мы начнем выполнять f2(5) вызов, имя a станет связанным с аргументом 5, и имя b станет привязанным к значению по умолчанию 1.

Но затем мы выполняем оператор присваивания. b появляется в левой части оператора присваивания, поэтому мы связываем имя b. Сначала Python вырабатывает b + 1, то есть 6, затем связывает b с этим значением. Теперь b привязано к значению 6. Но значение по умолчанию для функции не было затронуто : 1 по-прежнему равно 1!


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

Вероятно, также стоит указать на хитрый случай. Правило, которое я дал выше (о присваивании, всегда связывающем имена без влияния на значение, поэтому, если что-то еще влияет на имя, оно должно сделать это путем изменения значения), верно для стандартного присваивания, но не всегда для «расширенных» операторов присваивания как +=, -= и *=.

Что они делают, к сожалению, зависит от того, на чем вы их используете. В:

x += y

это обычно ведет себя как:

x = x + y

т.е. он вычисляет новое значение с помощью и привязывает x к этому значению, не влияя на старое значение. Но если x является списком, то он фактически изменяет значение, на которое ссылается x! Так что будьте осторожны с этим делом.

0 голосов
/ 31 января 2012

В f1 вы сохраняете значение в массиве или, что еще лучше, в Python список, где, как в f2, вы работаете с переданными значениями. Это моя интерпретация. Я могу ошибаться

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...