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

Я пытаюсь понять подход 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.

  1. Когда вы определяете n и x в main, вы говорите им указывать на 2 объекта, а именно на 1 и [1,2,3]. Вот что делает =: он сообщает, на что должна указывать ваша переменная.

  2. Когда вы вызываете функцию f(n,x), вы указываете двум новым локальным переменным nf и xf указывать на те же два объекта, что и n и x.

  3. Когда вы используете "something"="anything_new", вы изменяете то, на что указывает "something". Когда вы используете .append, вы изменяете сам объект.

  4. Каким-то образом, несмотря на то, что вы дали им одинаковые имена, 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)

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

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