Acabei de perceber que em Python, se alguém escreve
for i in a:
i += 1
Os elementos da lista original a
não serão afetados, pois a variável i
acaba sendo apenas uma cópia do elemento original a
.
Para modificar o elemento original,
for index, i in enumerate(a):
a[index] += 1
seria necessário.
Fiquei realmente surpreso com esse comportamento. Isso parece ser muito contra-intuitivo, aparentemente diferente de outros idiomas e resultou em erros no meu código que eu tive que depurar por um longo tempo hoje.
Eu já li o Python Tutorial antes. Só para ter certeza, eu verifiquei o livro novamente agora, e ele nem sequer menciona esse comportamento.
Qual é o raciocínio por trás desse design? Espera-se que seja uma prática padrão em muitos idiomas, para que o tutorial acredite que os leitores devem obtê-la naturalmente? Em quais outros idiomas está presente o mesmo comportamento na iteração, ao qual devo prestar atenção no futuro?
i
for imutável ou se você estiver executando uma operação sem mutação. Com uma lista aninhadafor i in a: a.append(1)
teria um comportamento diferente; Python não copia as listas aninhadas. No entanto, números inteiros são imutáveis e a adição retorna um novo objeto, ele não altera o antigo.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. Mesmo em C #i = i + 1
afetara
?Respostas:
Eu já respondi a uma pergunta semelhante recentemente e é muito importante perceber que
+=
pode ter significados diferentes:Se o tipo de dados implementa adição no local (ou seja, possui uma
__iadd__
função funcionando corretamente ), os dados a quei
se refere são atualizados (não importa se estão em uma lista ou em outro lugar).Se o tipo de dados não implementa um
__iadd__
método para o qual ai += x
instrução é apenas sintáticai = i + x
, um novo valor é criado e atribuído ao nome da variáveli
.Se o tipo de dados é implementado,
__iadd__
mas faz algo estranho. É possível que seja atualizado ... ou não - isso depende do que é implementado lá.Python, números inteiros, flutuadores e seqüências de caracteres não são implementados,
__iadd__
portanto eles não serão atualizados no local. No entanto, outros tipos de dados comonumpy.array
ou oslist
implementam e se comportam como você esperava. Portanto, não é uma questão de cópia ou não cópia ao iterar (normalmente não faz cópias porlist
s etuple
s - mas isso também depende da implementação dos contêineres__iter__
e__getitem__
método!) - é mais uma questão de tipo de dados você armazenou no seua
.fonte
Esclarecimento - terminologia
Python não faz distinção entre os conceitos de referência e ponteiro . Eles geralmente usam apenas o termo referência , mas se você comparar com linguagens como C ++ que têm essa distinção - é muito mais próximo de um ponteiro .
Como o solicitante vem claramente do background do C ++, e como essa distinção - necessária para a explicação - não existe no Python, decidi usar a terminologia do C ++, que é:
void foo(int x);
é uma assinatura de uma função que recebe um número inteiro por valor .void foo(int* x);
é uma assinatura de uma função que recebe um número inteiro por ponteiro .void foo(int& x);
é uma assinatura de uma função que recebe um número inteiro por referência .O que você quer dizer com "diferente de outros idiomas"? A maioria dos idiomas que eu conheço que suporta cada loops está copiando o elemento, a menos que seja especificamente instruído de outra forma.
Especificamente para Python (embora muitos desses motivos possam se aplicar a outras linguagens com conceitos arquitetônicos ou filosóficos semelhantes):
Esse comportamento pode causar bugs para pessoas que não o conhecem, mas o comportamento alternativo pode causar bugs, mesmo para quem está ciente . Quando você atribui uma variável (
i
), geralmente não para e considera todas as outras variáveis que seriam alteradas por causa dela (a
). Limitar o escopo em que você está trabalhando é um fator importante na prevenção do código espaguete e, portanto, a iteração por cópia geralmente é o padrão, mesmo em idiomas que suportam a iteração por referência.As variáveis Python são sempre um único ponteiro, por isso é barato iterar por cópia - mais barato que iterar por referência, o que exigiria um adiamento extra sempre que você acessar o valor.
Python não tem o conceito de variáveis de referência como - por exemplo - C ++. Ou seja, todas as variáveis no Python são na verdade referências, mas no sentido de que são ponteiros - não uma referência constante nos bastidores, como
type& name
argumentos em C ++ . Como esse conceito não existe no Python, implementando a iteração por referência - sem falar em torná-lo o padrão! - exigirá adicionar mais complexidade ao bytecode.A
for
declaração do Python funciona não apenas em matrizes, mas em um conceito mais geral de geradores. Nos bastidores, o Python chamaiter
suas matrizes para obter um objeto que - quando você o chamanext
- retorna o próximo elemento ouraise
saStopIteration
. Existem várias maneiras de implementar geradores em Python, e teria sido muito mais difícil implementá-los para iteração por referência.fonte
*it = ...
- mas esse tipo de sintaxe já indica que você está modificando algo em outro lugar - o que torna a razão nº 1 menos um problema. Os motivos 2 e 3 também não se aplicam, porque em C ++ a cópia é cara e existe o conceito de variáveis de referência. Quanto ao motivo 4 - a capacidade de retornar uma referência permite uma implementação simples para todos os casos.Nenhuma das respostas aqui fornece código para você trabalhar para ilustrar realmente por que isso acontece no Python. E é divertido olhar para uma abordagem mais profunda, então aqui vai.
A principal razão pela qual isso não funciona como o esperado é porque no Python, quando você escreve:
não está fazendo o que você pensa que está fazendo. Inteiros são imutáveis. Isso pode ser visto quando você olha para o que o objeto realmente é no Python:
A função id representa um valor único e constante para um objeto em sua vida útil. Conceitualmente, ele mapeia livremente para um endereço de memória em C / C ++. Executando o código acima:
Isso significa que o primeiro
a
não é mais o mesmo que o segundoa
, porque seus IDs são diferentes. Efetivamente, eles estão em diferentes locais da memória.Com um objeto, no entanto, as coisas funcionam de maneira diferente. Substituí o
+=
operador aqui:A execução disso resulta na seguinte saída:
Observe que o atributo id nesse caso é realmente o mesmo para as duas iterações, mesmo que o valor do objeto seja diferente (você também pode encontrar o
id
valor int que o objeto mantém, o que mudaria conforme a mutação - porque números inteiros são imutáveis).Compare isso com quando você executa o mesmo exercício com um objeto imutável:
Isso gera:
Algumas coisas aqui para notar. Primeiro, no loop com o
+=
, você não está mais adicionando ao objeto original. Nesse caso, como ints estão entre os tipos imutáveis do Python , o python usa um ID diferente. Também é interessante notar que o Python usa o mesmo subjacenteid
para várias variáveis com o mesmo valor imutável:tl; dr - Python tem vários tipos imutáveis, que causam o comportamento que você vê. Para todos os tipos mutáveis, sua expectativa está correta.
fonte
@ A resposta de Idan explica muito bem por que o Python não trata a variável de loop como um ponteiro da maneira que você pode em C, mas vale a pena explicar mais detalhadamente como os trechos de código são descompactados, como no Python muitos bits de aparência simples de código serão na verdade chamadas para métodos internos . Para dar o seu primeiro exemplo
Há duas coisas para descompactar: a
for _ in _:
sintaxe e a_ += _
sintaxe. Para pegar o loop for primeiro, como outras linguagens, o Python possui umfor-each
loop que é essencialmente a sintaxe do açúcar para um padrão de iterador. No Python, um iterador é um objeto que define um.__next__(self)
método que retorna o elemento atual na sequência, avança para o próximo e aumentará umStopIteration
quando não houver mais itens na sequência. Um Iterable é um objeto que define um.__iter__(self)
método que retorna um iterador.(NB: an
Iterator
também é anIterable
e retorna a partir de seu.__iter__(self)
método.)O Python geralmente terá uma função embutida que delega para o método de sublinhado duplo personalizado. Portanto, tem o
iter(o)
que resolveo.__iter__()
e onext(o)
que resolveo.__next__()
. Observe que essas funções embutidas geralmente tentam uma definição padrão razoável se o método que eles delegariam não estiver definido. Por exemplo,len(o)
geralmente resolve,o.__len__()
mas se esse método não for definido, ele tentaráiter(o).__len__()
.Um loop é essencialmente definido em termos de
next()
,iter()
e mais estruturas básicas de controlo. Em geral, o códigoserá descompactado para algo como
Então, neste caso
é descompactado para
A outra metade disso é
i += 1
. Em geral,%ASSIGN% += %EXPR%
é descompactado%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Aqui__iadd__(self, other)
coloca a adição e retorna a si mesma.(NB: esse é outro caso em que o Python escolherá uma alternativa se o método principal não for definido. Se o objeto não for implementado,
__iadd__
ele voltará a funcionar__add__
. Ele realmente faz isso neste caso comoint
não é implementado__iadd__
- o que faz sentido porque são imutáveis e, portanto, não podem ser modificados no local.)Portanto, seu código aqui parece
onde podemos definir
Há um pouco mais no seu segundo pedaço de código. As duas coisas novas que precisamos saber são:
%ARG%[%KEY%] = %VALUE%
descompactar(%ARG%).__setitem__(%KEY%, %VALUE%)
e%ARG%[%KEY%]
descompactar(%ARG%).__getitem__(%KEY%)
. Juntando esse conhecimento,a[ix] += 1
descompactamosa.__setitem__(ix, a.__getitem__(ix).__add__(1))
(novamente:__add__
e não__iadd__
porque__iadd__
não é implementado pelo ints). Nosso código final se parece com:Para realmente responder sua pergunta a respeito de porque o primeiro não modificar a lista, enquanto o segundo faz, no nosso primeiro trecho que estamos recebendo
i
denext(_a_iter)
que meiosi
será umint
. Comoint
não pode ser modificado no local,i += 1
não faz nada na lista. No nosso segundo caso, não estamos modificando novamenteint
a lista, mas modificando a lista chamando__setitem__
.A razão para todo este exercício elaborado é porque acho que ensina a seguinte lição sobre Python:
Os métodos de sublinhado duplo são um obstáculo ao iniciar, mas são essenciais para apoiar a reputação de "pseudocódigo executável" do Python. Um programador decente de Python terá um entendimento completo desses métodos e de como eles são chamados e os definirá sempre que fizer sentido.
Edit : @deltab corrigiu meu uso desleixado do termo "coleção".
fonte
__len__
e__contains__
+=
funciona de maneira diferente com base no valor atual ser mutável ou imutável . Esse foi o principal motivo pelo qual demorou muito tempo para ser implementado no Python, pois os desenvolvedores do Python temiam que isso fosse confuso.Se
i
for um int, ele não poderá ser alterado, pois as entradas são imutáveis e, portanto, se o valor dasi
alterações for necessário, ele deve necessariamente apontar para outro objeto:No entanto, se o lado esquerdo é mutável , + = pode realmente alterá-lo; como se fosse uma lista:
No seu loop for,
i
refere-se a cada elemento dea
sua vez. Se esses são números inteiros, o primeiro caso se aplica e o resultado dei += 1
deve ser que ele se refere a outro objeto inteiro. A lista, éa
claro, ainda tem os mesmos elementos que sempre teve.fonte
i = 1
definidoi
como um objeto inteiro imutável,i = []
deverá ser definidoi
como um objeto de lista imutável. Em outras palavras, por que os objetos inteiros são imutáveis e os objetos de lista são mutáveis? Não vejo lógica por trás disso.list
implementa métodos que alteram seu conteúdo,int
não.[]
é um objeto de lista mutável ei = []
vamos fazeri
referência a esse objeto.+=
operador / método para se comportar de maneira semelhante (princípio da menor surpresa) para ambos os tipos: altere o objeto original ou retorne uma cópia modificada para números inteiros e listas.+=
é surpreendente em Python, mas considerou-se que as outras opções mencionadas também seriam surpreendentes, ou pelo menos menos práticas (alterar o objeto original não pode ser feito com o tipo de valor mais comum você usa + = com, ints. E copiar uma lista inteira é muito mais cara do que modificá-la, o Python não copia coisas como listas e dicionários, a menos que seja explicitamente solicitado). Foi um grande debate na época.O loop aqui é meio irrelevante. Assim como os parâmetros ou argumentos das funções, configurar um loop for como esse é essencialmente apenas uma atribuição de aparência sofisticada.
Inteiros são imutáveis. A única maneira de modificá-los é criando um novo número inteiro e atribuindo-o ao mesmo nome que o original.
A semântica do Python para atribuição é mapeada diretamente para os C's (sem surpresa, considerando os ponteiros PyObject * do CPython), com as únicas ressalvas de que tudo é um ponteiro e você não tem permissão para ter ponteiros duplos. Considere o seguinte código:
O que acontece? Imprime
1
. Por quê? Na verdade, é aproximadamente equivalente ao seguinte código C:No código C, é óbvio que o valor de não
a
é afetado completamente.Quanto ao motivo pelo qual as listas parecem funcionar, a resposta é basicamente o que você está atribuindo ao mesmo nome. As listas são mutáveis. A identidade do objeto nomeado
a[0]
será alterada, masa[0]
ainda é um nome válido. Você pode verificar isso com o seguinte código:Mas isso não é especial para listas. Substitua
a[0]
nesse código pory
e você obterá exatamente o mesmo resultado.fonte