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

182

Я пытаюсь понять подход Python к переменной области. В этом примере, почему f()может изменить значение x, как воспринимается внутри main(), но не значение n?

def f(n, x):
    n = 2
    x.append(4)
    print('In f():', n, x)

def main():
    n = 1
    x = [0,1,2,3]
    print('Before:', n, x)
    f(n, x)
    print('After: ', n, x)

main()

Вывод:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]
FMC
источник
7
хорошо объяснено здесь nedbatchelder.com/text/names.html
Роушан

Ответы:

212

Некоторые ответы содержат слово «копировать» в контексте вызова функции. Я нахожу это запутанным.

Python не копирует объекты вы проходите во время вызова функции когда - либо .

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

Объекты могут быть изменяемыми (например, списки) или неизменяемыми (например, целые числа, строки в Python). Изменчивый объект вы можете изменить. Вы не можете изменить имя, вы можете просто привязать его к другому объекту.

Ваш пример касается не областей или пространств имен , а именования, привязки и изменчивости объекта в Python.

def f(n, x): # these `n`, `x` have nothing to do with `n` and `x` from main()
    n = 2    # put `n` label on `2` balloon
    x.append(4) # call `append` method of whatever object `x` is referring to.
    print('In f():', n, x)
    x = []   # put `x` label on `[]` ballon
    # x = [] has no effect on the original list that is passed into the function

Вот хорошие картинки о разнице между переменными в других языках и именами в Python .

JFS
источник
3
Эта статья помогла мне лучше понять проблему и предлагает обходной путь и некоторые дополнительные способы использования: Значения параметров по умолчанию в Python
Gfy
@Gfy, я видел подобные примеры раньше, но для меня это не описывает реальную ситуацию. Если вы изменяете что-то, что было передано, не имеет смысла назначать это по умолчанию.
Марк Рэнсом
@MarkRansom, я думаю , что это имеет смысл , если вы хотите , чтобы обеспечить дополнительный выход назначения , как в: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Януш Ленар
В последней строке кода Себастьяна сказано: «# выше не влияет на первоначальный список». Но, на мой взгляд, это не влияет только на «n», но изменило «x» в функции main (). Я прав?
user17670 26.09.15
1
@ user17670: x = []in f()не влияет на список xв основной функции. Я обновил комментарий, чтобы сделать его более конкретным.
JFS
15

У вас уже есть несколько ответов, и я в целом согласен с Дж. Ф. Себастьяном, но вы можете найти это полезным в качестве ярлыка:

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

Каждый раз, когда вы видите, varname.foo()вы вызываете метод varname. Метод может изменить varname (например list.append). varname(или, скорее, объект с varnameименами) может существовать в нескольких областях, и, поскольку это один и тот же объект, любые изменения будут видны во всех областях.

[обратите внимание, что globalключевое слово создает исключение для первого случая]

Джон Фухи
источник
13

fфактически не изменяет значение x(которое всегда является одной и той же ссылкой на экземпляр списка). Скорее, это изменяет содержимое этого списка.

В обоих случаях копия ссылки передается в функцию. Внутри функции,

  • nполучает новое значение. Изменяется только ссылка внутри функции, а не та, что за ее пределами.
  • xне присваивается новое значение: ни ссылка внутри, ни снаружи функции не изменяются. Вместо этого, x«s значение изменяется.

Поскольку как xвнутри функции, так и снаружи она ссылается на одно и то же значение, оба видят изменение. Напротив, nвнутренняя функция и внешняя часть ссылаются на разные значения после nпереназначения внутри функции.

Конрад Рудольф
источник
8
«копия» вводит в заблуждение. В Python нет переменных вроде C. Все имена в Python являются ссылками. Вы не можете изменить имя, вы можете просто привязать его к другому объекту, вот и все. Имеет смысл говорить о непостоянных и неизменных объектах в Python, а не об именах.
JFS
1
@JF Себастьян: Ваше утверждение в лучшем случае вводит в заблуждение. Бесполезно думать о числах как о ссылках.
Питару,
9
@dysfunctor: числа являются ссылками на неизменяемые объекты. Если вы предпочитаете думать о них по-другому, у вас есть куча странных особых случаев, которые нужно объяснить. Если вы думаете о них как о неизменных, особых случаев нет.
S.Lott
@ S.Lott: Независимо от того, что происходит под капотом, Гвидо ван Россум приложил немало усилий для разработки Python, чтобы программист мог воспринимать числа как просто ... числа.
Питару,
1
@JF, ссылка скопирована.
habnabit
7

Я переименую переменные, чтобы уменьшить путаницу. n -> nf или nmain . x -> xf или xmain :

def f(nf, xf):
    nf = 2
    xf.append(4)
    print 'In f():', nf, xf

def main():
    nmain = 1
    xmain = [0,1,2,3]
    print 'Before:', nmain, xmain
    f(nmain, xmain)
    print 'After: ', nmain, xmain

main()

Когда вы вызываете функцию f , среда выполнения Python создает копию xmain и назначает ее для xf , а также назначает копию nmain для nf .

В случае n копируемое значение равно 1.

В случае x копируемое значение не является литеральным списком [0, 1, 2, 3] . Это ссылка на этот список. xf и xmain указывают на один и тот же список, поэтому при изменении xf вы также изменяете xmain .

Если, однако, вы должны были написать что-то вроде:

    xf = ["foo", "bar"]
    xf.append(4)

вы обнаружите, что xmain не изменился. Это потому, что в строке xf = ["foo", "bar"] вы изменили xf, чтобы указать на новый список. Любые изменения, внесенные вами в этот новый список, не окажут влияния на список, на который по- прежнему указывает xmain .

Надеюсь, это поможет. :-)

Pitarou
источник
2
«В случае n, значение, которое копируется ...» - это неправильно, здесь копирование не выполняется (если не считать ссылки). Вместо этого python использует «имена», которые указывают на реальные объекты. nf и xf указывают на nmain и xmain, пока nf = 2имя не nfбудет изменено 2. Числа неизменны, списки изменчивы.
Кейси Кубалл
2

Это потому, что список является изменяемым объектом. Вы не устанавливаете x в значение [0,1,2,3], вы определяете метку для объекта [0,1,2,3].

Вы должны объявить вашу функцию f () следующим образом:

def f(n, x=None):
    if x is None:
        x = []
    ...
Луис Дамим
источник
3
Это не имеет ничего общего с изменчивостью. Если бы вы сделали x = x + [4]вместо x.append(4), вы бы не увидели никаких изменений в вызывающей стороне, хотя список является изменяемым. Это связано с тем, действительно ли оно мутировало.
glglgl
1
OTOH, если вы это сделаете, x += [4]то xмутирует, точно так же, как это происходит с тем x.append(4), чтобы вызывающий абонент увидел изменение.
PM 2Ring
2

n - это int (неизменяемый), и копия передается функции, поэтому в функции вы изменяете копию.

X является списком (изменяемым), и копия указателя передается функции, поэтому x.append (4) изменяет содержимое списка. Однако, если вы сказали, что x = [0,1,2,3,4] в своей функции, вы не измените содержимое x в main ().

Джейсон Кун
источник
3
Смотрите формулировку «копия указателя». Оба места получают ссылки на объекты. n является ссылкой на неизменный объект; x является ссылкой на изменяемый объект.
S.Lott
2

Если функции переписаны с совершенно разными переменными, и мы называем их id , это хорошо иллюстрирует ситуацию . Сначала я этого не понял и прочитал пост jfs с отличным объяснением , поэтому попытался понять / убедить себя:

def f(y, z):
    y = 2
    z.append(4)
    print ('In f():             ', id(y), id(z))

def main():
    n = 1
    x = [0,1,2,3]
    print ('Before in main:', n, x,id(n),id(x))
    f(n, x)
    print ('After in main:', n, x,id(n),id(x))

main()
Before in main: 1 [0, 1, 2, 3]   94635800628352 139808499830024
In f():                          94635800628384 139808499830024
After in main: 1 [0, 1, 2, 3, 4] 94635800628352 139808499830024

z и x имеют одинаковый идентификатор. Просто разные теги для той же базовой структуры, что и в статье.

jouell
источник
0

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

Сравните эти две функции

def foo(x):
    x[0] = 5

def goo(x):
    x = []

Теперь, когда вы вводите в оболочку

>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]

Сравните это с липучкой.

>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]

В первом случае мы передаем копию адреса cow в foo и foo изменяет состояние находящегося там объекта. Объект модифицируется.

Во втором случае вы передаете копию адреса коровы в Goo. Затем Goo приступает к изменению этой копии. Эффект: нет.

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

Объяснение устраняет много путаницы. Python передает адреса переменных, хранящиеся по значению.

ncmathsadist
источник
Чистая передача по значению указателя не очень отличается от передачи по ссылке, если вы правильно об этом думаете ...
galinette
Посмотри на липучку. Если бы вы просто передавали по ссылке, это изменило бы его аргумент. Нет, Python не является чистым языком передачи по ссылке. Он передает ссылки по значению.
ncmathsadist
0

Python копируется по значению ссылки. Объект занимает поле в памяти, и ссылка связана с этим объектом, но сама занимает поле в памяти. И имя / значение связано со ссылкой. В функции python всегда копируется значение ссылки, поэтому в вашем коде n копируется в новое имя, и когда вы его присваиваете, в стеке вызывающей стороны появляется новое пространство. Но для списка также скопировано имя, но оно относится к той же памяти (поскольку вы никогда не назначаете списку новое значение). Это магия в питоне!

sunxd
источник
0

Мое общее понимание состоит в том, что любая переменная объекта (например, список или dict) может быть изменена с помощью ее функций. Я полагаю, что вы не можете сделать это переназначить параметр - т.е. назначить его по ссылке в вызываемой функции.

Это согласуется со многими другими языками.

Запустите следующий короткий скрипт, чтобы увидеть, как он работает:

def func1(x, l1):
    x = 5
    l1.append("nonsense")

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
Борис Эпштейн
источник
-3

Я изменил свой ответ множество раз и понял, что мне не нужно ничего говорить, Python уже объяснил.

a = 'string'
a.replace('t', '_')
print(a)
>>> 'string'

a = a.replace('t', '_')
print(a)
>>> 's_ring'

b = 100
b + 1
print(b)
>>> 100

b = b + 1
print(b)
>>> 101

def test_id(arg):
    c = id(arg)
    arg = 123
    d = id(arg)
    return

a = 'test ids'
b = id(a)
test_id(a)
e = id(a)

# b = c  = e != d
# this function do change original value
del change_like_mutable(arg):
    arg.append(1)
    arg.insert(0, 9)
    arg.remove(2)
    return

test_1 = [1, 2, 3]
change_like_mutable(test_1)



# this function doesn't 
def wont_change_like_str(arg):
     arg = [1, 2, 3]
     return


test_2 = [1, 1, 1]
wont_change_like_str(test_2)
print("Doesn't change like a imutable", test_2)

Этот дьявол не является ссылкой / значением / изменяемым или нет / экземпляром, пространством имен или переменной / списком или строкой, ЭТО СИНТАКС, РАВНЫЙ ЗНАК.

HoD
источник