Por que uma função pode modificar alguns argumentos como percebidos pelo chamador, mas não outros?

182

Estou tentando entender a abordagem do Python para o escopo variável. Neste exemplo, por que é f()possível alterar o valor de x, conforme percebido dentro main(), mas não o valor de 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()

Resultado:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]
FMc
fonte
7
bem explicado aqui nedbatchelder.com/text/names.html
Roushan

Respostas:

212

Algumas respostas contêm a palavra "cópia" no contexto de uma chamada de função. Acho confuso.

Python não copiar objetos que você passa durante uma chamada de função que nunca .

Parâmetros de função são nomes . Quando você chama uma função, o Python vincula esses parâmetros a qualquer objeto que você passar (por meio de nomes no escopo do chamador).

Os objetos podem ser mutáveis ​​(como listas) ou imutáveis ​​(como números inteiros, seqüências de caracteres em Python). Objeto mutável que você pode alterar. Você não pode alterar um nome, apenas pode vinculá-lo a outro objeto.

Seu exemplo não é sobre escopos ou namespaces , é sobre nomeação, vinculação e mutabilidade de um objeto no 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

Aqui estão boas fotos sobre a diferença entre variáveis ​​em outros idiomas e nomes em Python .

jfs
fonte
3
Este artigo me ajudou a entender melhor o problema e sugere uma solução alternativa e alguns usos avançados: Padrão Valores de Parâmetros em Python
GFY
@ Gfy, já vi exemplos semelhantes antes, mas para mim não descreve uma situação do mundo real. Se você está modificando algo que é passado, não faz sentido atribuir um padrão.
Mark Ransom
@MarkRansom, eu acho que faz sentido se você deseja fornecer destino de saída opcional como em: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Janusz Lenar
Para a última linha do código de Sebastian, ele dizia "# o acima não tem efeito na lista original". Mas, na minha opinião, apenas não tem efeito em "n", mas alterou o "x" na função main (). Estou correcto?
user17670
1
@ user17670: x = []in f()não tem efeito na lista xna função principal. Atualizei o comentário para torná-lo mais específico.
JFS
15

Você já tem várias respostas, e eu concordo amplamente com JF Sebastian, mas você pode achar isso útil como um atalho:

A qualquer momento varname =, você cria uma nova ligação de nome no escopo da função. Qualquer valor que varnamefoi vinculado antes é perdido dentro desse escopo .

Sempre que vir que varname.foo()você está chamando um método varname. O método pode alterar varname (por exemplo list.append). varname(ou melhor, o objeto que varnamenomeia) pode existir em mais de um escopo e, como é o mesmo objeto, qualquer alteração será visível em todos os escopos.

[observe que a globalpalavra - chave cria uma exceção para o primeiro caso]

John Fouhy
fonte
13

fna verdade, não altera o valor de x(que é sempre a mesma referência a uma instância de uma lista). Em vez disso, altera o conteúdo desta lista.

Nos dois casos, uma cópia de uma referência é passada para a função. Dentro da função,

  • nrecebe um novo valor. Somente a referência dentro da função é modificada, não a fora dela.
  • xnão recebe um novo valor: nem a referência dentro nem fora da função é modificada. Em vez disso, xo valor de é modificado.

Como tanto a xfunção interna quanto a externa se referem ao mesmo valor, as duas vêem a modificação. Por outro lado, o ninterior da função e o exterior referem-se a diferentes valores após serem nreatribuídos dentro da função.

Konrad Rudolph
fonte
8
"cópia" é enganosa. Python não possui variáveis ​​como C. Todos os nomes no Python são referências. Você não pode modificar o nome, apenas pode vinculá-lo a outro objeto, só isso. Só faz sentido falar sobre objetos mutáveis ​​e imutáveis em Python, não são nomes.
jfs
1
@JF Sebastian: Sua declaração é enganosa na melhor das hipóteses. Não é útil pensar nos números como sendo referências.
Pitarou 22/02/09
9
@ disfunctor: os números são referências a objetos imutáveis. Se você preferir pensar nelas de outra maneira, você tem vários casos especiais para explicar. Se você os considera imutáveis, não há casos especiais.
224/09 S.Lott
@ S.Lott: Independentemente do que está acontecendo sob o capô, Guido van Rossum se esforçou bastante ao projetar o Python para que o programador considere os números como sendo apenas ... números.
Pitarou 22/02/09
1
@JF, a referência é copiada.
habnabit 22/02/09
7

Vou renomear variáveis ​​para reduzir a confusão. n -> nf ou nmain . x -> xf ou 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()

Quando você chama a função f , o tempo de execução do Python faz uma cópia do xmain e a atribui ao xf , e da mesma forma atribui uma cópia do nmain para nf .

No caso de n , o valor que é copiado é 1.

No caso de x, o valor copiado não é a lista literal [0, 1, 2, 3] . É uma referência a essa lista. xf e xmain estão apontando para a mesma lista; portanto, quando você modifica o xf, também está modificando o xmain .

Se, no entanto, você escrever algo como:

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

você descobriria que xmain não mudou. Isso ocorre porque, na linha xf = ["foo", "bar"], você alterou xf para apontar para uma nova lista. Quaisquer alterações feitas nessa nova lista não terão efeitos na lista para a qual o xmain ainda aponta.

Espero que ajude. :-)

Pitarou
fonte
2
"No caso de n, o valor que é copiado ..." - Isso está errado, não há cópia feita aqui (a menos que você conte as referências). Em vez disso, o python usa 'nomes' que apontam para os objetos reais. nf e xf apontam para nmain e xmain, até nf = 2, onde o nome nfé alterado para apontar 2. Os números são imutáveis, as listas são mutáveis.
Casey Kuball 19/04/12
2

É porque uma lista é um objeto mutável. Você não está definindo x para o valor de [0,1,2,3], está definindo um rótulo para o objeto [0,1,2,3].

Você deve declarar sua função f () assim:

def f(n, x=None):
    if x is None:
        x = []
    ...
Luiz Damim
fonte
3
Não tem nada a ver com mutabilidade. Se você preferir, em x = x + [4]vez de x.append(4), também não verá alterações no chamador, embora a lista seja mutável. Tem a ver com se é realmente mutado.
glglgl
1
OTOH, se você o fizer x += [4], xserá alterado, exatamente como acontece com x.append(4)o chamador, para que o chamador veja a alteração.
PM 2Ring
2

n é um int (imutável) e uma cópia é passada para a função, portanto, na função você está alterando a cópia.

X é uma lista (mutável) e uma cópia do ponteiro é passada para a função para que x.append (4) altere o conteúdo da lista. No entanto, você disse que x = [0,1,2,3,4] em sua função, não alteraria o conteúdo de x em main ().

Jason Coon
fonte
3
Assista à frase "cópia do ponteiro". Ambos os locais obtêm referências aos objetos. n é uma referência a um objeto imutável; x é uma referência a um objeto mutável.
224/09 S.Lott
2

Se as funções são reescritas com variáveis ​​completamente diferentes e chamamos id , isso ilustra bem o ponto. Eu não entendi isso no começo e li o post do jfs com a grande explicação , então tentei entender / me convencer:

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

zex têm o mesmo ID. Apenas tags diferentes para a mesma estrutura subjacente que o artigo diz.

jouell
fonte
0

Python é uma linguagem pura de passagem por valor, se você pensar da maneira certa. Uma variável python armazena a localização de um objeto na memória. A variável Python não armazena o próprio objeto. Quando você passa uma variável para uma função, está passando uma cópia do endereço do objeto que está sendo apontado pela variável.

Contraste essas duas funções

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

def goo(x):
    x = []

Agora, quando você digita no shell

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

Compare isso com gosma.

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

No primeiro caso, passamos uma cópia do endereço de cow para foo e foo modificou o estado do objeto que ali reside. O objeto é modificado.

No segundo caso, você passa uma cópia do endereço da vaca para a gosma. Então goo passa a alterar essa cópia. Efeito: nenhum.

Eu chamo isso de princípio da casa rosa . Se você fizer uma cópia do seu endereço e pedir ao pintor para pintar a casa naquele endereço de rosa, você terminará com uma casa rosa. Se você der uma cópia do seu endereço ao pintor e pedir para ele mudar para um novo endereço, o endereço da sua casa não será alterado.

A explicação elimina muita confusão. Python passa o endereço que as variáveis ​​armazenam por valor.

ncmathsadist
fonte
Um passe puro por valor de ponteiro não é muito diferente de uma passagem por referência se você pensar sobre isso da maneira certa ...
galinette
Olhe para gosma. Se você fosse puro passar por referência, isso teria mudado de argumento. Não, o Python não é uma linguagem pura de passagem por referência. Passa referências por valor.
Ncmathsadist
0

Python é copiar pelo valor de referência. Um objeto ocupa um campo na memória e uma referência é associada a esse objeto, mas ele próprio ocupa um campo na memória. E o nome / valor está associado a uma referência. Na função python, ele sempre copia o valor da referência; portanto, no seu código, n é copiado para ser um novo nome; quando você o atribui, ele possui um novo espaço na pilha de chamadas. Mas para a lista, o nome também foi copiado, mas se refere à mesma memória (já que você nunca atribui à lista um novo valor). Isso é uma mágica em python!

sunxd
fonte
0

Meu entendimento geral é que qualquer variável de objeto (como uma lista ou um ditado, entre outras) pode ser modificada por meio de suas funções. O que eu acredito que você não é capaz de fazer é reatribuir o parâmetro - ou seja, atribuí-lo por referência dentro de uma função que pode ser chamada.

Isso é consistente com muitos outros idiomas.

Execute o seguinte script curto para ver como funciona:

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

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
Boris Epstein
fonte
-3

Eu havia modificado minha resposta várias vezes e percebi que não precisava dizer nada, o python já havia se explicado.

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)

Este diabo não é a referência / valor / mutável ou não / instância, espaço de nome ou variável / lista ou str, É A SINTAXE, SINAL DE IGUALDADE.

HoD
fonte