Dlaczego funkcja może modyfikować niektóre argumenty postrzegane przez wywołującego, a inne nie?


182

Próbuję zrozumieć podejście Pythona do zmiennego zakresu. W tym przykładzie, dlaczego jest w f()stanie zmienić wartość xpostrzeganą wewnątrz main(), ale nie wartość 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()

Wynik:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]

7
dobrze wyjaśniono tutaj nedbatchelder.com/text/names.html
Roushan,

Odpowiedzi:


212

Niektóre odpowiedzi zawierają słowo „kopiuj” w kontekście wywołania funkcji. Uważam to za zagmatwane.

Python nie kopiuje obiektów ty przechodzą podczas wywołania funkcji kiedykolwiek .

Parametry funkcji to nazwy . Kiedy wywołujesz funkcję, Python wiąże te parametry z dowolnymi przekazywanymi obiektami (przez nazwy w zakresie obiektu wywołującego).

Obiekty mogą być zmienne (jak listy) lub niezmienne (jak liczby całkowite, łańcuchy w Pythonie). Zmienny obiekt, który możesz zmienić. Nie możesz zmienić nazwy, po prostu możesz powiązać ją z innym obiektem.

Twój przykład nie dotyczy zakresów ani przestrzeni nazw , ale dotyczy nazewnictwa, wiązania i zmienności obiektu w Pythonie.

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

Oto ładne zdjęcia przedstawiające różnicę między zmiennymi w innych językach a nazwami w Pythonie .


3
Ten artykuł pomógł mi lepiej zrozumieć problem i sugeruje obejście i niektóre zaawansowane zastosowania: Domyślne wartości parametrów w Pythonie
Gfy.

@Gfy, widziałem już podobne przykłady, ale dla mnie nie opisuje to rzeczywistej sytuacji. Jeśli modyfikujesz coś, co zostało przekazane, nie ma sensu nadawać temu wartości domyślnej.
Mark Ransom

@MarkRansom, myślę, że to ma sens, jeśli chcesz, aby zapewnić opcjonalny cel wyjście na przykład: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Janusz Lenar

W ostatnim wierszu kodu Sebastiana było napisane „# powyższe nie ma wpływu na oryginalną listę”. Ale moim zdaniem nie ma to wpływu tylko na „n”, ale zmieniło „x” w funkcji main (). Mam rację?
user17670

1
@ user17670: x = []in f()nie ma wpływu na listę xw funkcji głównej. Zaktualizowałem komentarz, aby był bardziej szczegółowy.
jfs

15

Masz już wiele odpowiedzi i zasadniczo zgadzam się z JF Sebastianem, ale może ci się to przydać jako skrót:

Za każdym razem, gdy widzisz varname =, tworzysz nowe powiązanie nazwy w zakresie funkcji. Jakakolwiek wartość varnamebyła wcześniej związana, zostaje utracona w tym zakresie .

Za każdym razem, gdy widzisz, varname.foo()że wywołujesz metodę varname. Metoda może zmienić nazwę zmiennej (np list.append.). varname(a raczej obiekt, którego varnamenazwa) może istnieć w więcej niż jednym zakresie, a ponieważ jest to ten sam obiekt, wszelkie zmiany będą widoczne we wszystkich zakresach.

[zwróć uwagę, że globalsłowo kluczowe tworzy wyjątek od pierwszego przypadku]


13

fw rzeczywistości nie zmienia wartości x(która jest zawsze tym samym odwołaniem do instancji listy). Raczej zmienia zawartość tej listy.

W obu przypadkach kopia odwołania jest przekazywana do funkcji. Wewnątrz funkcji

  • notrzymuje nową wartość. Modyfikowane jest tylko odwołanie wewnątrz funkcji, a nie poza nią.
  • xnie otrzymuje nowej wartości: ani referencja wewnątrz, ani na zewnątrz funkcji nie są modyfikowane. Zamiast tego, xjest to wartość jest modyfikowana.

Ponieważ zarówno xwewnątrz funkcji, jak i na zewnątrz, odnoszą się do tej samej wartości, obie zobacz modyfikację. Z kolei nwewnątrz funkcji i na zewnątrz odnoszą się do innych wartości po nponownym przypisaniu w funkcji.


8
„kopia” jest myląca. Python nie ma zmiennych takich jak C. Wszystkie nazwy w Pythonie są referencjami. Nie możesz zmienić nazwy, po prostu możesz powiązać ją z innym obiektem, to wszystko. Mówienie o zmiennym i niezmiennym obiekcie ma sens tylko w Pythonie, a nie są to nazwy.
jfs

1
@JF Sebastian: Twoje stwierdzenie jest w najlepszym razie mylące. Myślenie o liczbach jako o odniesieniach nie jest przydatne.
Pitarou

9
@dysfunctor: liczby to odwołania do niezmiennych obiektów. Jeśli wolisz myśleć o nich w inny sposób, musisz wyjaśnić kilka dziwnych, specjalnych przypadków. Jeśli uważasz, że są niezmienne, nie ma specjalnych przypadków.
S.Lott

@ S.Lott: Niezależnie od tego, co się dzieje pod maską, Guido van Rossum włożył wiele wysiłku w zaprojektowanie Pythona, aby programista mógł myśleć o liczbach jako o… liczbach.
Pitarou

1
@JF, odniesienie jest kopiowane.
habnabit

7

Zmienię nazwy zmiennych, aby zmniejszyć zamieszanie. n -> nf lub nmain . x -> xf lub 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()

Kiedy wywołujesz funkcję f , środowisko uruchomieniowe Pythona tworzy kopię xmain i przypisuje ją do xf oraz podobnie przypisuje kopię nmain do nf .

W przypadku n kopiowana wartość to 1.

W przypadku x kopiowana wartość nie jest listą literałów [0, 1, 2, 3] . Jest to odniesienie do tej listy. xf i xmain wskazują na tę samą listę, więc modyfikując xf , modyfikujesz również xmain .

Gdybyś jednak miał napisać coś takiego:

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

przekonasz się, że xmain się nie zmienił. Dzieje się tak, ponieważ w wierszu xf = ["foo", "bar"] zmieniono xf, aby wskazywał na nową listę. Wszelkie zmiany wprowadzone na nowej liście nie będą miały wpływu na listę, na którą xmain nadal wskazuje.

Mam nadzieję, że to pomoże. :-)


2
„W przypadku n, wartość, która jest kopiowana…” - to jest błędne, nie ma tu żadnego kopiowania (chyba że liczysz referencje). Zamiast tego Python używa „nazw”, które wskazują na rzeczywiste obiekty. nf i xf wskazują na nmain i xmain, aż do miejsca nf = 2, w którym nazwa nfzostanie zmieniona na wskazującą 2. Liczby są niezmienne, listy są zmienne.
Casey Kuball

2

Dzieje się tak, ponieważ lista jest zmiennym obiektem. Nie ustawiasz x na wartość [0,1,2,3], definiujesz etykietę dla obiektu [0,1,2,3].

Powinieneś zadeklarować swoją funkcję f () w ten sposób:

def f(n, x=None):
    if x is None:
        x = []
    ...

3
Nie ma to nic wspólnego ze zmiennością. Jeśli chcesz zrobić x = x + [4]zamiast x.append(4), to widać żadnych zmian rozmówcy, a także, choć lista jest zmienny. Ma to związek z tym, czy rzeczywiście jest zmutowany.
glglgl

1
OTOH, jeśli nie x += [4]następnie xjest zmutowany, tak jak co dzieje się z x.append(4)tak dzwoniący będzie widać zmiany.
2:00 po południu,

2

n jest liczbą int (niezmienną), a kopia jest przekazywana do funkcji, więc w funkcji zmieniasz kopię.

X jest listą (zmienną), a kopia wskaźnika jest przekazywana do funkcji, więc x.append (4) zmienia zawartość listy. Jednak powiedziałeś x = [0,1,2,3,4] w swojej funkcji, nie zmieniłbyś zawartości x w main ().


3
Obserwuj frazę „kopia wskaźnika”. Oba miejsca mają odniesienia do obiektów. n jest odniesieniem do niezmiennego obiektu; x jest odniesieniem do zmiennego obiektu.
S.Lott

2

Jeśli funkcje są przepisywane z zupełnie innymi zmiennymi i nazywamy je id , to dobrze ilustruje to sprawę. Nie zrozumiałem tego na początku i przeczytałem post jfs ze świetnym wyjaśnieniem , więc starałem się zrozumieć / przekonać siebie:

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

zi x mają ten sam identyfikator. Tylko różne tagi dla tej samej podstawowej struktury, jak mówi artykuł.


0

Python jest czystym językiem przekazywania wartości, jeśli myślisz o tym we właściwy sposób. Zmienna Pythona przechowuje lokalizację obiektu w pamięci. Zmienna Pythona nie przechowuje samego obiektu. Przekazując zmienną do funkcji, przekazujesz kopię adresu obiektu wskazywanego przez zmienną.

Porównaj te dwie funkcje

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

def goo(x):
    x = []

Teraz, kiedy piszesz do powłoki

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

Porównaj to z goo.

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

W pierwszym przypadku przekazujemy kopię adresu krowy do foo i foo modyfikuje stan znajdującego się tam obiektu. Obiekt zostanie zmodyfikowany.

W drugim przypadku przekazujesz kopię adresu krowy do mazi. Następnie goo przystępuje do zmiany tej kopii. Efekt: brak.

Nazywam to zasadą różowego domu . Jeśli skopiujesz swój adres i powiesz malarzowi, żeby pomalował dom pod tym adresem na różowo, skończysz z różowym domem. Jeśli przekażesz malarzowi kopię swojego adresu i powiesz mu, aby zmienił go na nowy adres, adres twojego domu się nie zmieni.

Wyjaśnienie eliminuje wiele nieporozumień. Python przekazuje zmienne adresów przechowywane według wartości.


Czysta wartość wskaźnika pass by nie różni się zbytnio od przebiegu przez odniesienie, jeśli myślisz o tym we właściwy sposób ...
galinette

Spójrz na maź. Gdybyś był czystym przekazem przez odniesienie, zmieniłby swój argument. Nie, Python nie jest czystym językiem przekazywania przez odniesienie. Przekazuje odwołania według wartości.
ncmathsadist

0

Python jest kopiowany przez wartość odniesienia. Obiekt zajmuje pole w pamięci i odniesienie jest skojarzone z tym obiektem, ale samo zajmuje pole w pamięci. Nazwa / wartość jest powiązana z odniesieniem. W funkcji Pythona zawsze kopiuje wartość odwołania, więc w twoim kodzie n jest kopiowane jako nowa nazwa, a kiedy ją przypisujesz, ma nowe miejsce na stosie wywołań. Ale w przypadku listy nazwa również została skopiowana, ale odnosi się do tej samej pamięci (ponieważ nigdy nie przypisujesz liście nowej wartości). To jest magia w Pythonie!


0

Moje ogólne zrozumienie jest takie, że każda zmienna obiektowa (taka jak między innymi lista lub dykt) może być modyfikowana za pomocą jej funkcji. Uważam, że nie jesteś w stanie ponownie przypisać parametru - tj. Przypisać go przez odniesienie w ramach wywoływalnej funkcji.

Jest to zgodne z wieloma innymi językami.

Uruchom następujący krótki skrypt, aby zobaczyć, jak to działa:

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

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)

-3

Wiele razy modyfikowałem odpowiedź i zdałem sobie sprawę, że nie muszę nic mówić, Python już się wyjaśnił.

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)

Ten diabeł nie jest odniesieniem / wartością / zmienną lub nie / instancją, przestrzenią nazw lub zmienną / listą lub str. TO JEST SKŁADNIA, ZNAK RÓWNOŚCI.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.