Qual é o método de concatenação de cadeias mais eficiente em python?

148

Existe algum método eficiente de concatenação de cadeias de massa em Python (como StringBuilder em C # ou StringBuffer em Java)? Encontrei os seguintes métodos aqui :

  • Concatenação simples usando +
  • Usando lista de strings e join método de
  • Usando UserStringdeMutableStringmódulo
  • Usando matriz de caracteres e o array módulo
  • Usando cStringIOdo StringIOmódulo

Mas o que os especialistas usam ou sugerem e por quê?

[ Uma pergunta relacionada aqui ]

mshsayem
fonte
1
Pergunta semelhante: stackoverflow.com/questions/476772
Peter Mortensen
Para concatenar fragmentos conhecidos em um, o Python 3.6 terá f''seqüências de formato que serão mais rápidas do que quaisquer alternativas nas versões anteriores do Python.
Antti Haapala

Respostas:

127

Você pode estar interessado nisso: Uma anedota de otimização de Guido. Embora valha a pena lembrar também que este é um artigo antigo e é anterior à existência de coisas como ''.join(embora eu achestring.joinfields seja mais ou menos o mesmo)

Com base nisso, o arraymódulo pode ser mais rápido se você conseguir resolver o problema. Mas ''.joinprovavelmente é rápido o suficiente e tem o benefício de ser idiomático e, portanto, mais fácil para outros programadores de python entenderem.

Finalmente, a regra de ouro da otimização: não otimize, a menos que você saiba que precisa, e meça em vez de adivinhar.

Você pode medir diferentes métodos usando o timeitmódulo. Isso pode lhe dizer qual é o mais rápido, em vez de estranhos aleatórios na internet fazendo suposições.

John Fouhy
fonte
1
Querendo acrescentar ao ponto sobre quando otimizar: não se esqueça de testar os piores casos. Por exemplo, posso aumentar minha amostra para que meu código atual passe de 0,17 segundos para 170 segundos. Bem, eu quero testar em amostras maiores, pois há menos variação lá.
Flipper
2
"Não otimize até que você saiba que precisa." A menos que você esteja apenas usando um idioma nominalmente diferente e possa evitar o retrabalho do seu código com pouco esforço extra.
precisa saber é o seguinte
1
Um lugar que você sabe que precisa é a entrevista (que é sempre um ótimo momento para aprimorar sua compreensão profunda). Infelizmente eu não encontrei nenhum artigo moderno sobre isso. (1) O Java / C # String ainda é tão ruim em 2017? (2) E o C ++? (3) Agora fale sobre o melhor e o mais recente do Python, focando nos casos em que precisamos fazer milhões de concatenações. Podemos confiar que a união funcionaria em tempo linear?
user1854182
O que significa "rápido o suficiente" .join()? A questão principal é: a) cria uma cópia da string para concatenação (semelhante a s = s + 'abc'), que requer tempo de execução O (n) ou b) simplesmente anexa à string existente sem criar uma cópia, o que requer O (1) ?
CGFoX 30/10/19
64

''.join(sequenceofstrings) é o que geralmente funciona melhor - mais simples e rápido.

Alex Martelli
fonte
3
@mshsayem, em Python, uma sequência pode ser qualquer objeto enumerável, até mesmo uma função.
Nick Dandoulakis
2
Eu absolutamente amo o ''.join(sequence)idioma. É especialmente útil produzir listas separadas por vírgula: ', '.join([1, 2, 3])fornece a sequência '1, 2, 3'.
Andrew Keeton
7
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- neste caso, o argumento para ingressar é um iterador, criado através de uma expressão geradora. Não existe uma lista temporária que seja construída.
balpha
2
@balpha: e ainda a versão do gerador é mais lenta que a versão de compreensão da lista: C: \ temp> python -mtimeit "'' .join (chr (x) para x em xrange (65,91))" 100000 loops, melhor de 3: 9,71 usec por loop C: \ temp> python -mtimeit "'' .join ([chr (x) para x in xrange (65,91)])" "100000 loops, melhor de 3: 7,1 usec por loop
hughdbrown
1
@hughdbrown, sim, quando você tem memória livre, o listcomp do wazoo (timeit timeit típico) pode ser melhor otimizado do que o genexp, geralmente de 20 a 30%. Quando as coisas apertadas de memória são diferentes - difícil de reproduzir em timeit, embora! -)
Alex Martelli
58

O Python 3.6 mudou o jogo para concatenação de string de componentes conhecidos com Interpolação de String Literal .

Dado o caso de teste da resposta de mkoistinen , ter strings

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'

Os candidatos são

  • f'http://{domain}/{lang}/{path}'- 0,151 µs

  • 'http://%s/%s/%s' % (domain, lang, path) - 0,321 µs

  • 'http://' + domain + '/' + lang + '/' + path - 0,356 µs

  • ''.join(('http://', domain, '/', lang, '/', path))- 0,249 µs (observe que a construção de uma tupla de comprimento constante é um pouco mais rápida que a construção de uma lista de comprimento constante).

Portanto, atualmente, o código mais curto e mais bonito possível também é o mais rápido.

Nas versões alfa do Python 3.6, a implementação de f''strings era a mais lenta possível - na verdade, o código de bytes gerado é praticamente equivalente ao ''.join()caso de chamadas desnecessárias parastr.__format__ quais, sem argumentos, retornariaself inalterado. Essas ineficiências foram tratadas antes da 3.6 final.

A velocidade pode ser contrastada com o método mais rápido do Python 2, que é a +concatenação no meu computador; e isso leva 0,203 µs com cadeias de 8 bits e 0,259 µs se as cadeias forem todas Unicode.

Antti Haapala
fonte
38

Depende do que você está fazendo.

Após o Python 2.5, a concatenação de strings com o operador + é bastante rápida. Se você está apenas concatenando alguns valores, usar o operador + funciona melhor:

>>> x = timeit.Timer(stmt="'a' + 'b'")
>>> x.timeit()
0.039999961853027344

>>> x = timeit.Timer(stmt="''.join(['a', 'b'])")
>>> x.timeit()
0.76200008392333984

No entanto, se você estiver montando uma string em um loop, será melhor usar o método de junção de lista:

>>> join_stmt = """
... joined_str = ''
... for i in xrange(100000):
...   joined_str += str(i)
... """
>>> x = timeit.Timer(join_stmt)
>>> x.timeit(100)
13.278000116348267

>>> list_stmt = """
... str_list = []
... for i in xrange(100000):
...   str_list.append(str(i))
... ''.join(str_list)
... """
>>> x = timeit.Timer(list_stmt)
>>> x.timeit(100)
12.401000022888184

... mas observe que é necessário reunir um número relativamente alto de strings antes que a diferença se torne perceptível.

Jason Baker
fonte
2
1) Na sua primeira medição, provavelmente é a construção da lista que leva o tempo. Tente com uma tupla. 2) executa CPython uniformemente boa, no entanto, outras implementações Python executar forma pior com + e + =
u0b34a0f6ae
22

De acordo com a resposta de John Fouhy, não otimize a menos que você precise, mas se você está aqui e faz essa pergunta, pode ser precisamente porque você precisa . No meu caso, eu precisava montar alguns URLs a partir de variáveis ​​de string ... rápido. Percebi que ninguém (até agora) parece considerar o método de formato de string, então pensei em tentar isso e, principalmente por um interesse moderado, pensei em lançar o operador de interpolação de string para um bom medidor. Para ser sincero, não achei que um desses fosse empilhar em uma operação direta '+' ou em '' .join (). Mas adivinhem? No meu sistema Python 2.7.5, o operador de interpolação de strings governa todos eles e string.format () é o pior executor:

# concatenate_test.py

from __future__ import print_function
import timeit

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'
iterations = 1000000

def meth_plus():
    '''Using + operator'''
    return 'http://' + domain + '/' + lang + '/' + path

def meth_join():
    '''Using ''.join()'''
    return ''.join(['http://', domain, '/', lang, '/', path])

def meth_form():
    '''Using string.format'''
    return 'http://{0}/{1}/{2}'.format(domain, lang, path)

def meth_intp():
    '''Using string interpolation'''
    return 'http://%s/%s/%s' % (domain, lang, path)

plus = timeit.Timer(stmt="meth_plus()", setup="from __main__ import meth_plus")
join = timeit.Timer(stmt="meth_join()", setup="from __main__ import meth_join")
form = timeit.Timer(stmt="meth_form()", setup="from __main__ import meth_form")
intp = timeit.Timer(stmt="meth_intp()", setup="from __main__ import meth_intp")

plus.val = plus.timeit(iterations)
join.val = join.timeit(iterations)
form.val = form.timeit(iterations)
intp.val = intp.timeit(iterations)

min_val = min([plus.val, join.val, form.val, intp.val])

print('plus %0.12f (%0.2f%% as fast)' % (plus.val, (100 * min_val / plus.val), ))
print('join %0.12f (%0.2f%% as fast)' % (join.val, (100 * min_val / join.val), ))
print('form %0.12f (%0.2f%% as fast)' % (form.val, (100 * min_val / form.val), ))
print('intp %0.12f (%0.2f%% as fast)' % (intp.val, (100 * min_val / intp.val), ))

Os resultados:

# python2.7 concatenate_test.py
plus 0.360787868500 (90.81% as fast)
join 0.452811956406 (72.36% as fast)
form 0.502608060837 (65.19% as fast)
intp 0.327636957169 (100.00% as fast)

Se eu usar um domínio mais curto e um caminho mais curto, a interpolação ainda vence. A diferença é mais acentuada, porém, com cordas mais longas.

Agora que eu tinha um bom script de teste, também testei no Python 2.6, 3.3 e 3.4, eis os resultados. No Python 2.6, o operador positivo é o mais rápido! No Python 3, a participação vence. Nota: esses testes são muito repetíveis no meu sistema. Portanto, 'plus' é sempre mais rápido no 2.6, 'intp' é sempre mais rápido no 2.7 e 'join' é sempre mais rápido no Python 3.x.

# python2.6 concatenate_test.py
plus 0.338213920593 (100.00% as fast)
join 0.427221059799 (79.17% as fast)
form 0.515371084213 (65.63% as fast)
intp 0.378169059753 (89.43% as fast)

# python3.3 concatenate_test.py
plus 0.409130576998 (89.20% as fast)
join 0.364938726001 (100.00% as fast)
form 0.621366866995 (58.73% as fast)
intp 0.419064424001 (87.08% as fast)

# python3.4 concatenate_test.py
plus 0.481188605998 (85.14% as fast)
join 0.409673971997 (100.00% as fast)
form 0.652010936996 (62.83% as fast)
intp 0.460400978001 (88.98% as fast)

# python3.5 concatenate_test.py
plus 0.417167026084 (93.47% as fast)
join 0.389929617057 (100.00% as fast)
form 0.595661019906 (65.46% as fast)
intp 0.404455224983 (96.41% as fast)

Lição aprendida:

  • Às vezes, minhas suposições estão completamente erradas.
  • Teste contra o sistema env. você estará executando em produção.
  • A interpolação de cadeias ainda não está morta!

tl; dr:

  • Se você estiver usando o 2.6, use o operador +.
  • se você estiver usando o 2.7, use o operador '%'.
  • Se você estiver usando o 3.x, use '' .join ().
mkoistinen
fonte
2
Nota: a interpolação literal de strings é mais rápida ainda para 3.6+:f'http://{domain}/{lang}/{path}'
TemporalWolf
1
Além disso, .format()tem três formas, a fim de rápido para lento: "{}".format(x), "{0}".format(x),"{x}".format(x=x)
TemporalWolf
A verdadeira lição: quando o domínio do problema é pequeno, por exemplo, compondo seqüências curtas, o método geralmente não importa. E mesmo quando importa, por exemplo, você realmente está criando um milhão de strings, a sobrecarga geralmente importa mais. É um sintoma típico de se preocupar com o problema errado. Somente quando a sobrecarga não é significativa, por exemplo, ao criar o livro inteiro como uma sequência, a diferença de método começa a importar.
Hui Zhou
7

depende muito dos tamanhos relativos da nova string após cada nova concatenação. Com o +operador, para cada concatenação é criada uma nova string. Se as cadeias intermediárias forem relativamente longas, elas +se tornarão cada vez mais lentas porque a nova cadeia intermediária está sendo armazenada.

Considere este caso:

from time import time
stri=''
a='aagsdfghfhdyjddtyjdhmfghmfgsdgsdfgsdfsdfsdfsdfsdfsdfddsksarigqeirnvgsdfsdgfsdfgfg'
l=[]
#case 1
t=time()
for i in range(1000):
    stri=stri+a+repr(i)
print time()-t

#case 2
t=time()
for i in xrange(1000):
    l.append(a+repr(i))
z=''.join(l)
print time()-t

#case 3
t=time()
for i in range(1000):
    stri=stri+repr(i)
print time()-t

#case 4
t=time()
for i in xrange(1000):
    l.append(repr(i))
z=''.join(l)
print time()-t

Resultados

1 0.00493192672729

2 0.000509023666382

3 0.00042200088501

4 0.000482797622681

No caso de 1 e 2, adicionamos uma string grande e o join () executa cerca de 10 vezes mais rápido. Nos casos 3 e 4, adicionamos uma string pequena e o '+' executa um pouco mais rápido

David Bielen
fonte
3

Eu me deparei com uma situação em que precisava ter uma sequência anexável de tamanho desconhecido. Estes são os resultados de referência (python 2.7.3):

$ python -m timeit -s 's=""' 's+="a"'
10000000 loops, best of 3: 0.176 usec per loop
$ python -m timeit -s 's=[]' 's.append("a")'
10000000 loops, best of 3: 0.196 usec per loop
$ python -m timeit -s 's=""' 's="".join((s,"a"))'
100000 loops, best of 3: 16.9 usec per loop
$ python -m timeit -s 's=""' 's="%s%s"%(s,"a")'
100000 loops, best of 3: 19.4 usec per loop

Isso parece mostrar que '+ =' é o mais rápido. Os resultados do link skymind estão um pouco desatualizados.

(Percebo que o segundo exemplo não está completo, a lista final precisaria ser unida. Isso mostra, no entanto, que simplesmente preparar a lista leva mais tempo que a sequência concat.)

MattK
fonte
Estou recebendo um sub-segundo vezes para o 3º e 4º testes. Por que você está recebendo tempos tão altos? pastebin.com/qabNMCHS
bad_keypoints 02/08
@ronnieaka: Ele está subindo 1 segundo para todos os testes. Ele está recebendo> 1 µs para o 3º e 4º, o que você não obteve. Também tenho tempos mais lentos nesses testes (no Python 2.7.5, Linux). Pode ser CPU, versão, sinalizadores de construção, quem sabe.
Thanatos
Esses resultados de referência são inúteis. Especialmente, o primeiro caso, que não está executando nenhuma concatenação de sequência, retornando intacto o valor da segunda sequência.
Antti Haapala
3

Um ano depois, vamos testar a resposta de mkoistinen com o python 3.4.3:

  • mais 0,963564149000 (95,83% mais rápido)
  • join 0.923408469000 (100.00% mais rápido)
  • formulário 1.501130934000 (61,51% mais rápido)
  • intp 1.019677452000 (90,56% mais rápido)

Nada mudou. A associação ainda é o método mais rápido. Com o intp sendo a melhor escolha em termos de legibilidade, você pode querer usá-lo.

Ramsch
fonte
1
Talvez possa ser uma adição à resposta mkoistinen, pois está um pouco aquém da resposta completa (ou pelo menos adicione o código que você está usando).
Trilion
1

Inspirado nos benchmarks do @ JasonBaker, aqui está um simples que compara 10 "abcdefghijklmnopqrstuvxyz"strings, mostrando que.join() é mais rápido; mesmo com este pequeno aumento de variáveis:

Catenação

>>> x = timeit.Timer(stmt='"abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz"')
>>> x.timeit()
0.9828147209324385

Junte-se

>>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
>>> x.timeit()
0.6114138159765048
AT
fonte
Dê uma olhada na resposta aceita (role para baixo) desta pergunta: stackoverflow.com/questions/1349311/…
mshsayem
1

Para um pequeno conjunto de strings curtas (ou seja, 2 ou 3 strings com no máximo alguns caracteres), o plus ainda é muito mais rápido. Usando o maravilhoso script de mkoistinen em Python 2 e 3:

plus 2.679107467004 (100.00% as fast)
join 3.653773699996 (73.32% as fast)
form 6.594011374000 (40.63% as fast)
intp 4.568015249999 (58.65% as fast)

Portanto, quando seu código estiver executando um grande número de pequenas concatenações separadas, plus é a maneira preferida se a velocidade for crucial.

user7505681
fonte
1

Provavelmente "novas strings f no Python 3.6" são a maneira mais eficiente de concatenar strings.

Usando% s

>>> timeit.timeit("""name = "Some"
... age = 100
... '%s is %s.' % (name, age)""", number = 10000)
0.0029734770068898797

Usando .format

>>> timeit.timeit("""name = "Some"
... age = 100
... '{} is {}.'.format(name, age)""", number = 10000)
0.004015227983472869

Usando f

>>> timeit.timeit("""name = "Some"
... age = 100
... f'{name} is {age}.'""", number = 10000)
0.0019175919878762215

Fonte: https://realpython.com/python-f-strings/

Super Nova
fonte