Por que + = se comporta de maneira inesperada nas listas?

118

O +=operador em python parece estar operando inesperadamente nas listas. Alguém pode me dizer o que está acontecendo aqui?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

RESULTADO

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += barparece afetar todas as instâncias da classe, enquanto foo = foo + barparece se comportar da maneira que eu esperaria que as coisas se comportassem.

O +=operador é denominado "operador de atribuição composto".

eucalculia
fonte
veja a diferença entre 'estender' e 'anexar' na lista também
N 1.1
3
Não acho que isso mostre algo errado com Python. A maioria das linguagens nem mesmo permite que você use o +operador em matrizes. Acho que faz todo o sentido, neste caso +=, anexar.
Skilldrick
4
É chamado de 'atribuição aumentada', oficialmente.
Martijn Pieters

Respostas:

138

A resposta geral é que +=tenta chamar o __iadd__método especial e, se não estiver disponível, ele tenta usar __add__. Portanto, a questão é a diferença entre esses métodos especiais.

O __iadd__método especial é para uma adição no local, ou seja, altera o objeto sobre o qual atua. O __add__método especial retorna um novo objeto e também é usado para o +operador padrão .

Portanto, quando o +=operador é usado em um objeto que tem um __iadd__definido, o objeto é modificado no local. Caso contrário, ele tentará usar o plano __add__e retornar um novo objeto.

É por isso que para tipos mutáveis, como listas, +=o valor do objeto muda, enquanto para tipos imutáveis ​​como tuplas, strings e inteiros, um novo objeto é retornado ( a += btorna-se equivalente a a = a + b).

Para tipos que suporte tanto __iadd__e __add__, portanto, você tem que ter cuidado qual você usa. a += birá chamar __iadd__e sofrer mutação a, enquanto a = a + birá criar um novo objeto e atribuí-lo a a. Eles não são a mesma operação!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

Para tipos imutáveis ​​(onde você não tem __iadd__) a += be a = a + bsão equivalentes. Isso é o que permite que você use +=em tipos imutáveis, o que pode parecer uma decisão de design estranha até que você considere que, de outra forma, você não poderia usar +=em tipos imutáveis ​​como números!

Scott Griffiths
fonte
4
Também há um __radd__método que pode ser chamado às vezes (é relevante para expressões que envolvem principalmente subclasses).
jfs
2
Em perspectiva: + = é útil se a memória e a velocidade forem importantes
Norfeldt
3
Saber que +=realmente estende uma lista, isso explica por que x = []; x = x + {}dá um TypeErrortempo x = []; x += {}apenas retorna [].
zezollo
96

Para o caso geral, consulte a resposta de Scott Griffith . Ao lidar com listas como você, entretanto, o +=operador é uma abreviação de someListObject.extend(iterableObject). Veja a documentação de extend () .

A extendfunção acrescentará todos os elementos do parâmetro à lista.

Ao fazer isso, foo += somethingvocê está modificando a lista foono local, portanto, você não altera a referência para a qual o nome fooaponta, mas está alterando o objeto da lista diretamente. Com foo = foo + something, você está realmente criando uma nova lista.

Este código de exemplo irá explicar isso:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

Observe como a referência muda quando você reatribui a nova lista a l.

Como baré uma variável de classe em vez de uma variável de instância, a modificação no local afetará todas as instâncias dessa classe. Mas ao redefinir self.bar, a instância terá uma variável de instância separada self.barsem afetar as outras instâncias da classe.

AndiDog
fonte
7
Isso nem sempre é verdade: a = 1; a + = 1; é Python válido, mas ints não tem nenhum método "extend ()". Você não pode generalizar isso.
e-satis
2
Feito alguns testes, Scott Griffiths acertou, então -1 para você.
e-satis
11
@ e-statis: O OP estava falando claramente sobre listas, e eu declarei claramente que estou falando sobre listas também. Não estou generalizando nada.
AndiDog
Removido o -1, a resposta é boa o suficiente. Ainda acho que a resposta de Griffiths é melhor.
e-satis
A princípio, parece estranho pensar que a += bé diferente de a = a + bduas listas ae b. Mas faz sentido; extendseria mais frequentemente o que se pretendia fazer com as listas, em vez de criar uma nova cópia de toda a lista, que terá maior complexidade de tempo. Se os desenvolvedores precisarem ter cuidado para não modificar as listas originais no local, então as tuplas são uma opção melhor sendo objetos imutáveis. +=com tuplas não pode modificar a tupla original.
Pranjal Mittal
22

O problema aqui é que baré definido como um atributo de classe, não uma variável de instância.

Em foo, o atributo de classe é modificado no initmétodo, por isso todas as instâncias são afetadas.

Em foo2, uma variável de instância é definida usando o atributo de classe (vazio) e cada instância tem o seu próprio bar.

A implementação "correta" seria:

class foo:
    def __init__(self, x):
        self.bar = [x]

Claro, os atributos de classe são completamente legais. Na verdade, você pode acessá-los e modificá-los sem criar uma instância da classe como esta:

class foo:
    bar = []

foo.bar = [x]
Can Berk Güder
fonte
8

Existem duas coisas envolvidas aqui:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+operador chama o __add__método em uma lista. Ele pega todos os elementos de seus operandos e faz uma nova lista contendo esses elementos mantendo sua ordem.

+=o operador chama o __iadd__método da lista. Ele pega um iterável e anexa todos os elementos do iterável à lista no local. Ele não cria um novo objeto de lista.

Na aula, fooa declaração self.bar += [x]não é uma declaração de atribuição, mas na verdade se traduz em

self.bar.__iadd__([x])  # modifies the class attribute  

que modifica a lista no local e atua como o método de lista extend.

Na aula foo2, ao contrário, a instrução de atribuição no initmétodo

self.bar = self.bar + [x]  

pode ser desconstruída como:
A instância não tem nenhum atributo bar(embora haja um atributo de classe com o mesmo nome), portanto, ela acessa o atributo de classe bare cria uma nova lista anexando xa ela. A declaração se traduz em:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

Em seguida, ele cria um atributo de instância bare atribui a lista recém-criada a ele. Observe que barno rhs da atribuição é diferente do barlhs.

Para instâncias de classe foo, baré um atributo de classe e não um atributo de instância. Portanto, qualquer alteração no atributo de classe barserá refletida em todas as instâncias.

Pelo contrário, cada instância da classe foo2tem seu próprio atributo de instância, barque é diferente do atributo de classe do mesmo nome bar.

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

Espero que isso esclareça as coisas.

ajay
fonte
5

Embora muito tempo tenha passado e muitas coisas corretas tenham sido ditas, não há uma resposta que agrupe ambos os efeitos.

Você tem 2 efeitos:

  1. um comportamento "especial", talvez despercebido de listas com +=(conforme declarado por Scott Griffiths )
  2. o fato de que atributos de classe, bem como atributos de instância estão envolvidos (conforme afirmado por Can Berk Büder )

Em classe foo, o __init__método modifica o atributo de classe. É porque se self.bar += [x]traduz para self.bar = self.bar.__iadd__([x]). __iadd__()é para modificação local, portanto, ele modifica a lista e retorna uma referência a ela.

Observe que a instância dict é modificada, embora isso normalmente não seja necessário, pois a classe dict já contém a mesma atribuição. Portanto, esse detalhe passa quase despercebido - exceto se você fizer um foo.bar = []depois. Aqui as instâncias barpermanecem as mesmas graças ao fato mencionado.

Na aula foo2, entretanto, o da classe baré usado, mas não é tocado. Em vez disso, um [x]é adicionado a ele, formando um novo objeto, como self.bar.__add__([x])é chamado aqui, que não modifica o objeto. O resultado é colocado na instância dict então, dando à instância a nova lista como um dict, enquanto o atributo da classe permanece modificado.

A distinção entre ... = ... + ...e ... += ...afeta também as atribuições posteriores:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

Você pode verificar a identidade dos objetos com print id(foo), id(f), id(g)(não se esqueça dos ()s adicionais se estiver no Python3).

BTW: O +=operador é chamado de "atribuição aumentada" e geralmente se destina a fazer modificações no local, tanto quanto possível.

glglgl
fonte
5

As outras respostas parecem ter coberto, embora pareça valer a pena citá-las e referir-se às Tarefas Aumentadas PEP 203 :

Eles [os operadores de atribuição aumentada] implementam o mesmo operador que sua forma binária normal, exceto que a operação é feita "no local" quando o objeto do lado esquerdo a suporta e que o lado esquerdo é avaliado apenas uma vez.

...

A ideia por trás da atribuição aumentada em Python é que não é apenas uma maneira mais fácil de escrever a prática comum de armazenar o resultado de uma operação binária em seu operando à esquerda, mas também uma maneira para o operando à esquerda em questão saiba que ele deve operar `sobre si mesmo ', em vez de criar uma cópia modificada de si mesmo.

Mwardm
fonte
1
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])
tanglei
fonte
0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

Vemos que, quando tentamos modificar um objeto imutável (inteiro neste caso), o Python simplesmente nos dá um objeto diferente. Por outro lado, podemos fazer alterações em um objeto mutável (uma lista) e mantê-lo sempre o mesmo objeto.

ref: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Consulte também o url abaixo para entender a cópia rasa e profunda

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

Roshan ok
fonte
# ID é o mesmo para listas
roshan ok