Algum motivo para não usar '+' para concatenar duas strings?

123

Um antipadrão comum em Python é concatenar uma sequência de strings usando +um loop. Isso é ruim porque o intérprete Python precisa criar um novo objeto de seqüência de caracteres para cada iteração e acaba levando tempo quadrático. (Versões recentes do CPython aparentemente podem otimizar isso em alguns casos, mas outras implementações não podem, então os programadores são desencorajados a confiar nisso.) ''.joinÉ o caminho certo para fazer isso.

No entanto, eu ouvi dizer ( incluindo aqui no Stack Overflow ) que você nunca deve usar +para concatenação de strings, mas sempre usar ''.joinou uma string de formato. Não entendo por que esse é o caso se você está apenas concatenando duas strings. Se meu entendimento está correto, não deve levar tempo quadrático, e acho que a + bé mais limpo e mais legível que um ''.join((a, b))ou outro '%s%s' % (a, b).

É uma boa prática usar +para concatenar duas seqüências de caracteres? Ou existe um problema que não conheço?

Taymon
fonte
É mais limpo e você tem mais controle para não fazer concatenação. Mas seu ligeiramente mais lento, o comércio corda contusão off: P
Jakob Bowyer
Você está dizendo que +é mais rápido ou mais lento? E porque?
Taymon
1
+ É mais rápido, In [2]: %timeit "a"*80 + "b"*80 1000000 loops, best of 3: 356 ns per loop In [3]: %timeit "%s%s" % ("a"*80, "b"*80) 1000000 loops, best of 3: 907 ns per loop
Jakob Bowyer
4
In [3]: %timeit "%s%s" % (a, b) 1000000 loops, best of 3: 590 ns per loop In [4]: %timeit a + b 10000000 loops, best of 3: 147 ns per loop
Jakob Bowyer
1
@JakobBowyer e outros: O argumento "concatenação de string é ruim" não tem quase nada a ver com velocidade, mas aproveita a conversão automática de tipo com __str__. Veja minha resposta para exemplos.
Izkata

Respostas:

119

Não há nada errado em concatenar duas strings com +. Na verdade, é mais fácil ler do que ''.join([a, b]).

Você está certo de que concatenar mais de 2 strings com +é uma operação O (n ^ 2) (em comparação com O (n) para join) e, portanto, se torna ineficiente. No entanto, isso não tem a ver com o uso de um loop. Even a + b + c + ...é O (n ^ 2), o motivo é que cada concatenação produz uma nova string.

O CPython2.4 e superior tentam mitigar isso, mas ainda é aconselhável usar joinao concatenar mais de 2 strings.

ggozad
fonte
5
@ Mutant: .joinleva um iterável, então ambos .join([a,b])e .join((a,b))são válidos.
Foundling
1
Tempos interessantes sugerem o uso +ou +=a resposta aceita (de 2013) em stackoverflow.com/a/12171382/378826 (de Lennart Regebro), mesmo para o CPython 2.3+, e escolher apenas o padrão "acrescentar / associar" se este indicador mais claro expuser o idéia para a solução do problema em questão.
Diletante
49

O operador Plus é uma solução perfeitamente adequada para concatenar duas seqüências de caracteres Python. Mas se você continuar adicionando mais de duas strings (n> 25), poderá pensar em outra coisa.

''.join([a, b, c]) O truque é uma otimização de desempenho.

Mikko Ohtamaa
fonte
2
Uma tupla não seria melhor que uma lista?
ThiefMaster 06/04
7
A tupla seria mais rápida - o código era apenas um exemplo :) Normalmente, várias entradas de seqüência de caracteres longas são dinâmicas.
Mikko Ohtamaa
5
@martineau Eu acho que ele quer dizer gerar dinamicamente e adicionar append()strings a uma lista.
Peter C #
5
É preciso dizer aqui: a tupla geralmente é uma estrutura MAIS LENTA, especialmente se estiver crescendo. Com list você pode usar list.extend (list_of_items) e list.append (item), que são muito mais rápidos ao concatenar coisas dinamicamente.
Antti Haapala
6
+1 para n > 25. Os seres humanos precisam de pontos de referência para começar em algum lugar.
N611x007
8

A suposição de que nunca se deve usar + para concatenação de strings, mas sempre usar '' .join pode ser um mito. É verdade que o uso +cria cópias temporárias desnecessárias do objeto de sequência imutável, mas o outro fato não citado é que a chamada joinem um loop geralmente adicionaria a sobrecarga de function call. Vamos dar o seu exemplo.

Crie duas listas, uma a partir da questão SO vinculada e outra uma maior fabricada

>>> myl1 = ['A','B','C','D','E','F']
>>> myl2=[chr(random.randint(65,90)) for i in range(0,10000)]

Vamos criar duas funções UseJoine UsePlususar a respectiva joine a +funcionalidade.

>>> def UsePlus():
    return [myl[i] + myl[i + 1] for i in range(0,len(myl), 2)]

>>> def UseJoin():
    [''.join((myl[i],myl[i + 1])) for i in range(0,len(myl), 2)]

Permite executar o timeit com a primeira lista

>>> myl=myl1
>>> t1=timeit.Timer("UsePlus()","from __main__ import UsePlus")
>>> t2=timeit.Timer("UseJoin()","from __main__ import UseJoin")
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=100000)/100000)
2.48 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=100000)/100000)
2.61 usec/pass
>>> 

Eles têm quase o mesmo tempo de execução.

Vamos usar cProfile

>>> myl=myl2
>>> cProfile.run("UsePlus()")
         5 function calls in 0.001 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <pyshell#1376>:1(UsePlus)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("UseJoin()")
         5005 function calls in 0.029 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.015    0.015    0.029    0.029 <pyshell#1388>:1(UseJoin)
        1    0.000    0.000    0.029    0.029 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     5000    0.014    0.000    0.014    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {range}

E parece que o uso de Join resulta em chamadas de funções desnecessárias que podem aumentar a sobrecarga.

Agora voltando à pergunta. Devemos desencorajar o uso do +excesso joinem todos os casos?

Eu acredito que não, as coisas devem ser levadas em consideração

  1. Comprimento da string em questão
  2. Nº de operação de concatenação.

E, fora do curso, em uma otimização pré-amadurecida no desenvolvimento é ruim.

Abhijit
fonte
7
Obviamente, a idéia seria não usar joindentro do próprio loop - em vez disso, o loop geraria uma sequência que seria passada para a junção.
Jsbueno #
7

Ao trabalhar com várias pessoas, às vezes é difícil saber exatamente o que está acontecendo. O uso de uma string de formato em vez da concatenação pode evitar um incômodo específico que aconteceu várias vezes conosco:

Digamos, uma função requer um argumento e você a escreve esperando obter uma string:

In [1]: def foo(zeta):
   ...:     print 'bar: ' + zeta

In [2]: foo('bang')
bar: bang

Portanto, essa função pode ser usada com bastante frequência em todo o código. Seus colegas de trabalho talvez saibam exatamente o que fazem, mas não necessariamente estejam totalmente atualizados nas informações internas e talvez não saibam que a função espera uma string. E assim eles podem acabar com isso:

In [3]: foo(23)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

/home/izkata/<ipython console> in foo(zeta)

TypeError: cannot concatenate 'str' and 'int' objects

Não haveria problema se você apenas usasse uma string de formato:

In [1]: def foo(zeta):
   ...:     print 'bar: %s' % zeta
   ...:     
   ...:     

In [2]: foo('bang')
bar: bang

In [3]: foo(23)
bar: 23

O mesmo vale para todos os tipos de objetos que definem __str__, que também podem ser passados:

In [1]: from datetime import date

In [2]: zeta = date(2012, 4, 15)

In [3]: print 'bar: ' + zeta
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

TypeError: cannot concatenate 'str' and 'datetime.date' objects

In [4]: print 'bar: %s' % zeta
bar: 2012-04-15

Então, sim: se você pode usar uma string de formato, faça isso e aproveite o que o Python tem a oferecer.

Izkata
fonte
1
+1 para uma opinião dissidente bem fundamentada. Ainda acho que sou a favor +.
Taymon
1
Por que você não define o método foo como: print 'bar:' + str (zeta)?
EngineerWithJava54321
@ EngineerWithJava54321 Por exemplo, zeta = u"a\xac\u1234\u20ac\U00008000"- então você teria que usar print 'bar: ' + unicode(zeta)para garantir que não haja erros. %snão é certo, sem ter que pensar sobre isso, e é muito mais curto
Izkata
@ EngineerWithJava54321 Outros exemplos são menos relevantes aqui, mas, por exemplo, "bar: %s"podem ser traduzidos para "zrb: %s br"outro idioma. A %sversão será apenas o trabalho, mas a versão String-concat se tornaria uma bagunça para lidar com todos os casos e os seus tradutores que agora tem duas traduções independentes para lidar com
Izkata
Se eles não souberem o que é a implementação do foo, eles encontrarão esse erro com qualquer um def.
19/09/17
3

Eu fiz um teste rápido:

import sys

str = e = "a xxxxxxxxxx very xxxxxxxxxx long xxxxxxxxxx string xxxxxxxxxx\n"

for i in range(int(sys.argv[1])):
    str = str + e

e cronometrou:

mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  8000000
8000000 times

real    0m2.165s
user    0m1.620s
sys     0m0.540s
mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  16000000
16000000 times

real    0m4.360s
user    0m3.480s
sys     0m0.870s

Aparentemente, há uma otimização para o a = a + bcaso. Não exibe o tempo O (n ^ 2) como se pode suspeitar.

Portanto, pelo menos em termos de desempenho, o uso +é bom.

Michael Slade
fonte
3
Você pode comparar com o caso "ingressar" aqui. E há a questão de outras implementações do Python, como PyPy, Jython, IronPython, etc ...
jsbueno
3

De acordo com os documentos do Python, o uso de str.join () fornecerá consistência de desempenho em várias implementações do Python. Embora o CPython otimize o comportamento quadrático de s = s + t, outras implementações do Python podem não.

Detalhes da implementação do CPython : Se s e t são duas strings, algumas implementações do Python, como o CPython, geralmente podem executar uma otimização no local para atribuições do formato s = s + t ou s + = t. Quando aplicável, essa otimização torna o tempo de execução quadrático muito menos provável. Essa otimização depende da versão e da implementação. Para código sensível ao desempenho, é preferível usar o método str.join () que garante desempenho consistente de concatenação linear entre versões e implementações.

Tipos de sequência nos documentos Python (veja a nota de rodapé [6])

Duque
fonte
2

Eu uso o seguinte com python 3.8

string4 = f'{string1}{string2}{string3}'
Lucas Vazquez
fonte
0

'' .join ([a, b]) é a melhor solução que + .

Como o código deve ser escrito de uma maneira que não prejudique outras implementações do Python (PyPy, Jython, IronPython, Cython, Psyco e outras)

O formulário a + = b ou a = a + b é frágil mesmo no CPython e não está presente em implementações que não usam refcounting (a contagem de referência é uma técnica de armazenar o número de referências, ponteiros ou identificadores em um recurso, como um objeto, bloco de memória, espaço em disco ou outro recurso )

https://www.python.org/dev/peps/pep-0008/#programming-recommendations

muhammad ali e
fonte
1
a += bfunciona em todas as implementações do Python, mas em algumas delas leva tempo quadrático quando feito dentro de um loop ; a pergunta era sobre concatenação de strings fora de um loop.
Taymon