Почему функция может изменять некоторые аргументы так, как они воспринимаются вызывающей стороной, а не другие?
Я пытаюсь понять подход 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]
13 ответов
Некоторые ответы содержат слово "копировать" в контексте вызова функции. Я нахожу это запутанным.
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.
У вас уже есть несколько ответов, и я в целом согласен с Дж. Ф. Себастьяном, но вы можете найти это полезным в качестве ярлыка:
В любое время вы видите varname =
вы создаете новую привязку имени в области видимости функции. Независимо от стоимости varname
был обязан до того, как теряется в этой области.
В любое время вы видите varname.foo()
вы вызываете метод на varname
, Метод может изменить varname (например, list.append
). varname
(или, скорее, объект, который varname
Имена) могут существовать в нескольких областях, и поскольку это один и тот же объект, любые изменения будут видны во всех областях.
[обратите внимание, что global
ключевое слово создает исключение для первого случая]
f
на самом деле не меняет значение x
(которая всегда является одной и той же ссылкой на экземпляр списка). Скорее, это изменяет содержимое этого списка.
В обоих случаях копия ссылки передается в функцию. Внутри функции,
n
получает новое значение. Изменяется только ссылка внутри функции, а не та, что за ее пределами.x
не присваивается новое значение: ни ссылка внутри, ни снаружи функции не изменяются. Вместо,x
Значение изменено.
Поскольку оба x
внутри функции и за ее пределами ссылаются на одно и то же значение, оба видят модификацию. В отличие от n
внутри функции и вне ее относятся к различным значениям после n
был переназначен внутри функции.
Я переименую переменные, чтобы уменьшить путаницу. 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.
Надеюсь, это поможет.:-)
Если функции переписаны с совершенно разными переменными, и мы вызываем для них 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 имеют одинаковый идентификатор. Просто разные теги для той же базовой структуры, что и в статье.
Мое общее понимание состоит в том, что любая переменная объекта (например, список или dict) может быть изменена с помощью ее функций. Я считаю, что вы не можете сделать это переназначить параметр - т.е. назначить его по ссылке в вызываемой функции.
Это согласуется со многими другими языками.
Запустите следующий короткий скрипт, чтобы увидеть, как он работает:
def func1(x, l1):
x = 5
l1.append("nonsense")
y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
`
Это потому, что список является изменяемым объектом. Вы не устанавливаете x в значение [0,1,2,3], вы определяете метку для объекта [0,1,2,3].
Вы должны объявить свою функцию f() следующим образом:
def f(n, x=None):
if x is None:
x = []
...
Python копируется по значению ссылки. Объект занимает поле в памяти, и ссылка связана с этим объектом, но сама занимает поле в памяти. И имя / значение связано со ссылкой. В функции python всегда копируется значение ссылки, поэтому в вашем коде n копируется в новое имя, и когда вы его присваиваете, в стеке вызывающей стороны появляется новое пространство. Но для списка также скопировано имя, но оно относится к той же памяти (так как вы никогда не назначаете списку новое значение). Это магия в питоне!
n - это int (неизменяемый), и копия передается функции, поэтому в функции вы изменяете копию.
X является списком (изменяемым), и копия указателя передается функции, поэтому x.append(4) изменяет содержимое списка. Однако, если вы сказали, что x = [0,1,2,3,4] в своей функции, вы не измените содержимое x в main ().
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 передает адреса переменных, хранящиеся по значению.
Когда вы передаете команду n = 2 внутри функции, она находит место в памяти и помечает его как 2. Но если вы вызываете метод append, вы в основном ссылаетесь на местоположение x (независимо от значения) и выполняете некоторую операцию над что.
Как сказал Джоуэлл. Это вопрос того, что указывает на что, и я бы добавил, что это также вопрос разницы между тем, что делает =, и тем, что делает метод .append.
Когда вы определяете n и x в main, вы говорите им указывать на 2 объекта, а именно на 1 и [1,2,3]. Вот что делает =: он сообщает, на что должна указывать ваша переменная.
Когда вы вызываете функцию f(n,x), вы указываете двум новым локальным переменным nf и xf указывать на те же два объекта, что и n и x.
Когда вы используете "something"="anything_new", вы изменяете то, на что указывает "something". Когда вы используете .append, вы изменяете сам объект.
Каким-то образом, несмотря на то, что вы дали им одинаковые имена, n в main() и n в f() не являются одним и тем же объектом, они только изначально указывают на один и тот же объект (на самом деле то же самое касается x). Изменение того, на что указывает один из них, не повлияет на другой. Однако, если вместо этого вы внесете изменения в сам объект, это повлияет на обе переменные, поскольку они обе указывают на один и тот же, теперь измененный, объект.
Давайте проиллюстрируем разницу между методом .append и = без определения новой функции:
сравнивать
m = [1,2,3]
n = m # this tells n to point at the same object as m does at the moment
m = [1,2,3,4] # writing m = m + [4] would also do the same
print('n = ', n,'m = ',m)
к
m = [1,2,3]
n = m
m.append(4)
print('n = ', n,'m = ',m)
В первом коде будет напечатано n = [1,2,3] m = [1, 2, 3, 4], так как в 3-й строке вы изменили не объект [1,2,3], а вы сказали m указать на новый, другой объект (используя '='), в то время как n по-прежнему указывает на исходный объект.
Во втором коде будет напечатано n = [1, 2, 3, 4] m = [1, 2, 3, 4]. Это связано с тем, что здесь и m, и n по-прежнему указывают на один и тот же объект во всем коде, но вы изменили сам объект (на который указывает m) с помощью метода .append... Обратите внимание, что результатом второго кода будет то же самое, независимо от того, пишете ли вы m.append(4) или n.append(4) в 3-й строке.
Как только вы это поймете, единственная путаница, которая останется, состоит в том, чтобы действительно понять, что, как я уже сказал, n и x внутри вашей функции f() и в вашей main() НЕ совпадают, они только изначально указывают на одно и то же объект при вызове f().
Я много раз изменял свой ответ и понял, что мне не нужно ничего говорить, 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)
Этот дьявол не является ссылкой / значением / изменяемым или нет / экземпляром, пространством имен или переменной / списком или строкой, ЭТО СИНТАКСИС, РАВНЫЙ ЗНАК.