Por que b + = (4,) funciona eb = b + (4,) não funciona quando b é uma lista?


77

Se tomarmos b = [1,2,3]e se tentarmos:b+=(4,)

Ele retorna b = [1,2,3,4], mas se tentarmos fazer b = b + (4,)isso não funcionará.

b = [1,2,3]
b+=(4,) # Prints out b = [1,2,3,4]
b = b + (4,) # Gives an error saying you can't add tuples and lists

Eu esperava b+=(4,)falhar, pois você não pode adicionar uma lista e uma tupla, mas funcionou. Por isso, tentei b = b + (4,)obter o mesmo resultado, mas não funcionou.


4
Acredito que uma resposta pode ser encontrada aqui .
jochen


No começo, eu interpretei isso errado e tentei fechá-lo como muito amplo, depois o retraí. Então eu pensei que tinha que ser uma duplicata, mas não só não pude voltar a votar, como arranquei o cabelo tentando encontrar outras respostas como essas. : /
Karl Knechtel

Respostas:


70

O problema das perguntas "por que" é que geralmente elas podem significar várias coisas diferentes. Vou tentar responder a cada uma que acho que você possa ter em mente.

"Por que é possível funcionar de maneira diferente?" o que é respondido por, por exemplo, isto . Basicamente, +=tenta usar métodos diferentes do objeto: __iadd__(que só é verificado no lado esquerdo), vs __add__e __radd__("reverse reverse"), verificado no lado direito se o lado esquerdo não tiver __add__) para +.

"O que exatamente cada versão faz?" Em resumo, o list.__iadd__método faz a mesma coisa que list.extend(mas, devido ao design da linguagem, ainda há uma atribuição de volta).

Isso também significa, por exemplo, que

>>> a = [1,2,3]
>>> b = a
>>> a += [4] # uses the .extend logic, so it is still the same object
>>> b # therefore a and b are still the same list, and b has the `4` added
[1, 2, 3, 4]
>>> b = b + [5] # makes a new list and assigns back to b
>>> a # so now a is a separate list and does not have the `5`
[1, 2, 3, 4]

+, é claro, cria um novo objeto, mas requer explicitamente outra lista em vez de tentar extrair elementos de uma sequência diferente.

"Por que é útil + = fazer isso? É mais eficiente; o extendmétodo não precisa criar um novo objeto. É claro que isso tem alguns efeitos surpreendentes às vezes (como acima), e geralmente o Python não é realmente sobre eficiência , mas essas decisões foram tomadas há muito tempo.

"Qual é o motivo para não permitir adicionar listas e tuplas com +?" Veja aqui (obrigado, @ splash58); Uma ideia é que (tupla + lista) produza o mesmo tipo que (lista + tupla) e não está claro qual o tipo de resultado. +=não tem esse problema, porque a += bobviamente não deve alterar o tipo de a.


2
Oof, tudo bem. E as listas não usam |, então isso meio que arruina meu exemplo. Se eu pensar em um exemplo mais claro mais tarde, eu o troco.
Karl Knechtel

11
Btw |para conjuntos é um operador pendulares, mas +para listas não é. Por esse motivo, não acho que o argumento sobre a ambiguidade de tipo seja particularmente forte. Como o operador não comuta, por que exigir o mesmo para os tipos? Pode-se apenas concordar que o resultado tem o tipo de lhs. Por outro lado, ao restringir list + iterator, o desenvolvedor é incentivado a ser mais explícito sobre suas intenções. Se você deseja criar uma nova lista que contém o material a partir aprorrogado pelo material do bjá existe uma maneira de fazer isso: new = a.copy(); new += b. É mais uma linha, mas cristalina.
a_guest

A razão pela qual a += bse comporta de maneira diferente do que a = a + bnão é eficiência. Na verdade, Guido considerou o comportamento escolhido menos confuso. Imagine uma função recebendo uma lista acomo argumento e depois fazendo a += [1, 2, 3]. Essa sintaxe certamente parece estar modificando a lista no lugar, em vez de criar uma nova lista; portanto, foi tomada a decisão de que ela deveria se comportar de acordo com a intuição da maioria das pessoas sobre o resultado esperado. No entanto, o mecanismo também teve que trabalhar para tipos imutáveis ​​como ints, o que levou ao design atual.
Sven Marnach 10/10/19

Pessoalmente, acho que o design é realmente mais confuso do que simplesmente a += babreviar a = a + b, como Ruby fez, mas posso entender como chegamos lá.
Sven Marnach 10/10/19

21

Eles não são equivalentes:

b += (4,)

é uma abreviação de:

b.extend((4,))

enquanto +concatena listas, portanto:

b = b + (4,)

você está tentando concatenar uma tupla para uma lista


14

Quando você faz isso:

b += (4,)

é convertido para isso:

b.__iadd__((4,)) 

Sob o capô que ele chama b.extend((4,)), extendaceita um iterador e é por isso que isso também funciona:

b = [1,2,3]
b += range(2)  # prints [1, 2, 3, 0, 1]

mas quando você faz isso:

b = b + (4,)

é convertido para isso:

b = b.__add__((4,)) 

aceite apenas objeto de lista.


4

Nos documentos oficiais, para os tipos de sequência mutável, ambos:

s += t
s.extend(t)

são definidos como:

estende s- se ao conteúdo det

O que é diferente de ser definido como:

s = s + t    # not equivalent in Python!

Isso também significa que qualquer tipo de sequência funcionarát , incluindo uma tupla como no seu exemplo.

Mas também funciona para gamas e geradores! Por exemplo, você também pode fazer:

s += range(3)

3

Os operadores de atribuição "aumentada", como +=foram introduzidos no Python 2.0, lançado em outubro de 2000. O design e a lógica estão descritos no PEP 203 . Um dos objetivos declarados desses operadores era o suporte às operações no local. Escrita

a = [1, 2, 3]
a += [4, 5, 6]

deve atualizar a lista a no local . Isso importa se houver outras referências à lista a, por exemplo, quando afoi recebido como argumento de função.

No entanto, a operação nem sempre pode acontecer no local, pois muitos tipos de Python, incluindo números inteiros e seqüências de caracteres, são imutáveis , portanto, por exemplo, i += 1para um número inteiro inão é possível operar no local.

Em resumo, os operadores de atribuição aumentada deveriam trabalhar no local quando possível e criar um novo objeto de outra forma. Para facilitar esses objetivos de design, a expressão x += yfoi especificada para se comportar da seguinte maneira:

  • Se x.__iadd__definido, x.__iadd__(y)é avaliado.
  • Caso contrário, se x.__add__implementado, x.__add__(y)é avaliado.
  • Caso contrário, se y.__radd__implementado, y.__radd__(x)é avaliado.
  • Caso contrário, gere um erro.

O primeiro resultado obtido por esse processo será atribuído de volta a x(a menos que esse resultado seja o NotImplementedsingleton, nesse caso a pesquisa continua com a próxima etapa).

Esse processo permite que tipos que suportam modificações no local sejam implementados __iadd__(). Tipos que não suportam modificação no local não precisam adicionar novos métodos mágicos, já que o Python volta automaticamente ao essencial x = x + y.

Então, finalmente, vamos à sua pergunta real - por que você pode adicionar uma tupla a uma lista com um operador de atribuição aumentada. De memória, o histórico disso era mais ou menos assim: O list.__iadd__()método foi implementado para simplesmente chamar o list.extend()método já existente no Python 2.0. Quando os iteradores foram introduzidos no Python 2.1, o list.extend()método foi atualizado para aceitar iteradores arbitrários. O resultado final dessas mudanças foi o que my_list += my_tuplefuncionou a partir do Python 2.1. O list.__add__()método, no entanto, nunca deveria suportar iteradores arbitrários como o argumento da direita - isso foi considerado inadequado para uma linguagem fortemente tipada.

Pessoalmente, acho que a implementação de operadores aumentados acabou sendo um pouco complexa em Python. Tem muitos efeitos colaterais surpreendentes, por exemplo, este código:

t = ([42], [43])
t[0] += [44]

A segunda linha aumenta TypeError: 'tuple' object does not support item assignment, mas a operação é executada com êxito de qualquer maneira - tserá ([42, 44], [43])após a execução da linha que gera o erro.


Bravo! Ter uma referência ao PEP é especialmente útil. Adicionei um link na outra extremidade, a uma pergunta anterior do SO sobre o comportamento da lista na tupla. Quando olho para trás como era o Python antes da versão 2.3, parece praticamente inutilizável em comparação com hoje ... (e ainda tenho uma vaga memória de tentar e falhar ao conseguir 1,5 para fazer qualquer coisa útil em um Mac muito antigo)
Karl Knechtel

2

A maioria das pessoas espera que X + = Y seja equivalente a X = X + Y. De fato, o Python Pocket Reference (4ª ed) de Mark Lutz diz na página 57 "Os dois formatos a seguir são aproximadamente equivalentes: X = X + Y, X + = Y ". No entanto, as pessoas que especificaram o Python não os tornaram equivalentes. Possivelmente, esse foi um erro que resultará em horas de tempo de depuração por programadores frustrados enquanto o Python permanecer em uso, mas agora é exatamente como o Python é. Se X é um tipo de sequência mutável, X + = Y é equivalente a X.extend (Y) e não a X = X + Y.


> Possivelmente foi um erro que resultará em horas de tempo de depuração por programadores frustrados enquanto o Python permanecer em uso <- você realmente sofreu por causa disso? Você parece estar falando por experiência própria. Eu gostaria muito de ouvir sua história.
Veky 07/10/19

1

Como é explicado aqui , se arraynão implementar o __iadd__método, b+=(4,)isso seria apenas um atalho, b = b + (4,)mas obviamente não é, o mesmo arrayacontece com o __iadd__método. Aparentemente, a implementação do __iadd__método é algo como isto:

def __iadd__(self, x):
    self.extend(x)

No entanto, sabemos que o código acima não é a implementação real do __iadd__método, mas podemos assumir e aceitar que existe algo como o extendmétodo, que aceita tuppleentradas.

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.