Quando "i + = x" é diferente de "i = i + x" no Python?

212

Foi-me dito que +=pode ter efeitos diferentes da notação padrão de i = i +. Existe um caso em que i += 1seria diferente i = i + 1?

MarJamRob
fonte
7
+=age como extend()no caso de listas.
Ashwini Chaudhary 13/03/2013
12
@AshwiniChaudhary Essa é uma distinção bem sutil, considerando que i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]é True. Muitos desenvolvedores podem não perceber que isso id(i)muda para uma operação, mas não para a outra.
Kojiro
1
@kojiro - Embora seja uma distinção sutil, acho que é importante.
mgilson
@ mgilson é importante, e por isso senti que precisava de uma explicação. :)
kojiro
1
Pergunta relacionada sobre as diferenças entre os dois em Java: stackoverflow.com/a/7456548/245966
jakub.g

Respostas:

317

Isso depende inteiramente do objeto i.

+=chama o __iadd__método (se existir - recorrendo novamente __add__se não existir), enquanto +chama o __add__método 1 ou o __radd__método em alguns casos 2 .

Do ponto de vista da API, __iadd__é suposto ser usado para modificar objetos mutáveis no local (retornando o objeto que foi mutado), ao passo que __add__deve retornar uma nova instância de algo. Para objetos imutáveis , os dois métodos retornam uma nova instância, mas __iadd__colocam a nova instância no espaço para nome atual com o mesmo nome que a instância antiga. Isso é por que

i = 1
i += 1

parece aumentar i. Na realidade, você obtém um novo número inteiro e o atribui "em cima de" i- perdendo uma referência ao número inteiro antigo. Nesse caso, i += 1é exatamente o mesmo que i = i + 1. Mas, com a maioria dos objetos mutáveis, é uma história diferente:

Como um exemplo concreto:

a = [1, 2, 3]
b = a
b += [1, 2, 3]
print a  #[1, 2, 3, 1, 2, 3]
print b  #[1, 2, 3, 1, 2, 3]

comparado com:

a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

Observe como no primeiro exemplo, uma vez be areferenciar o mesmo objeto, quando eu uso +=em b, ele realmente muda b(e avê que a mudança também - Afinal de contas, ele está referenciando a mesma lista). No segundo caso, no entanto, quando eu faço b = b + [1, 2, 3], isso pega a lista que bestá fazendo referência e a concatena com uma nova lista [1, 2, 3]. Em seguida, ele armazena a lista concatenada no espaço para nome atual como b- Sem levar em conta qual bera a linha anterior.


1 Na expressão x + y, se x.__add__não for implementada ou se x.__add__(y)retorna NotImplemented e xe ytêm tipos diferentes , em seguida, x + ytenta chamar y.__radd__(x). Então, no caso em que você tem

foo_instance += bar_instance

se Foonão implementar __add__ou __iadd__então o resultado aqui é o mesmo que

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 Na expressão foo_instance + bar_instance, bar_instance.__radd__será tentado antes foo_instance.__add__ se o tipo de bar_instancefor uma subclasse do tipo de foo_instance(por exemplo issubclass(Bar, Foo)). O racional para isso é porque Baré em certo sentido um objeto "de alto nível" que Fooassim Bardeve obter a opção de substituir Fooo comportamento.

mgilson
fonte
18
Bem, +=chama , __iadd__ se existir , e volta a adicionar e refazer o contrário. É por isso que i = 1; i += 1funciona mesmo que não haja int.__iadd__. Mas além desse pequeno detalhe, ótimas explicações.
abarnert
4
@abarnert - eu sempre assumi que int.__iadd__acabou de ligar __add__. Estou feliz por ter aprendido algo novo hoje :).
mgilson
@abarnert - Acho que talvez seja completa , x + ychamadas y.__radd__(x)se x.__add__não existe (ou retornos NotImplementede xe ysão de tipos diferentes)
mgilson
Se você realmente quer ser completista, deve mencionar que o bit "se existe" passa pelos mecanismos getattr habituais, exceto por algumas peculiaridades com classes clássicas e pelos tipos implementados na API C. nb_inplace_addou sq_inplace_concat, e essas funções da API C têm requisitos mais rígidos do que os métodos dunder do Python e ... Mas não acho que isso seja relevante para a resposta. A principal distinção é que +=tenta fazer uma adição no local antes de voltar a agir como +, o que acho que você já explicou.
22813 abarnert #
Sim, suponho que você esteja certo ... Embora eu possa apenas recorrer à posição de que a API C não faz parte do python . É parte do Cpython :-P
mgilson 13/03
67

Sob as cobertas, i += 1faz algo assim:

try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

Enquanto i = i + 1faz algo parecido com isto:

i = i.__add__(1)

Essa é uma pequena simplificação excessiva, mas você entendeu: o Python fornece aos tipos uma maneira de lidar +=especialmente, criando um __iadd__método e um __add__.

A intenção é que tipos mutáveis, como list, se transformem em __iadd__(e retornem self, a menos que você esteja fazendo algo muito complicado), enquanto tipos imutáveis, como int, simplesmente não o implementarão.

Por exemplo:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

Porque l2é o mesmo objeto que l1, e você mutou l1, também modificou l2.

Mas:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

Aqui, você não sofreu mutação l1; em vez disso, você criou uma nova lista l1 + [3]e recuperou o nome l1para apontá-lo, deixando l2apontar para a lista original.

(Na +=versão, você também estava religando l1, é só nesse caso que você estava religando da mesma forma listque já estava vinculada, portanto, geralmente você pode ignorar essa parte.)

abarnert
fonte
se __iadd__realmente chamar __add__em caso de um AttributeError?
mgilson
Bem, i.__iadd__não liga __add__; é isso i += 1que chama __add__.
abarnert
errr ... Sim, foi isso que eu quis dizer. Interessante. Não sabia que isso era feito automaticamente.
mgilson
3
A primeira tentativa é realmente i = i.__iadd__(1)- iadd pode modificar o objeto no lugar, mas não precisa, e, portanto, espera-se retornar o resultado em ambos os casos.
LVC
Note que isto significa que operator.iaddas chamadas __add__sobre AttributeError, mas não pode religar o resultado ... então i=1; operator.iadd(i, 1)retorna 2 e folhas idefinido para 1. O que é um pouco confuso.
abarnert
6

Aqui está um exemplo que se compara diretamente i += xcom i = i + x:

def foo(x):
  x = x + [42]

def bar(x):
  x += [42]

c = [27]
foo(c); # c is not changed
bar(c); # c is changed to [27, 42]
Deqing
fonte