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]

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 .


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]


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.


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. :-)


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 = []
    ...

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 ().


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.


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.


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!


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)

-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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.