Concatenação de string vs. substituição de string em Python

98

Em Python, me escapa onde e quando usar concatenação de string versus substituição de string. Como a concatenação de strings sofreu grandes aumentos no desempenho, esta (se tornando mais) uma decisão estilística em vez de prática?

Para um exemplo concreto, como lidar com a construção de URIs flexíveis:

DOMAIN = 'http://stackoverflow.com'
QUESTIONS = '/questions'

def so_question_uri_sub(q_num):
    return "%s%s/%d" % (DOMAIN, QUESTIONS, q_num)

def so_question_uri_cat(q_num):
    return DOMAIN + QUESTIONS + '/' + str(q_num)

Edit: Também houve sugestões sobre como juntar uma lista de strings e para usar a substituição nomeada. São variantes do tema central, qual seja, qual é a maneira certa de fazê-lo e em que momento? Obrigado pelas respostas!

Gotgenes
fonte
Engraçado, em Ruby, a interpolação de strings é geralmente mais rápida do que a concatenação ...
Keltia
você esqueceu de retornar "" .join ([DOMAIN, QUESTIONS, str (q_num)])
Jimmy
Não sou especialista em Ruby, mas aposto que a interpolação é mais rápida porque as strings são mutáveis ​​em Ruby. Strings são sequências imutáveis ​​em Python.
gotgenes
1
apenas um pequeno comentário sobre URIs. URIs não são exatamente como strings. Existem URIs, portanto, você deve ter muito cuidado ao concatená-los ou compará-los. Exemplo: um servidor entregando suas representações por http na porta 80. example.org (sem slah no final) example.org/ (slash) example.org:80/ (slah + porta 80) são o mesmo uri, mas não são iguais corda.
karlcow

Respostas:

55

A concatenação é (significativamente) mais rápida de acordo com minha máquina. Mas, estilisticamente, estou disposto a pagar o preço da substituição se o desempenho não for crítico. Bem, e se eu precisar de formatação, não há necessidade nem de fazer a pergunta ... não há opção a não ser usar interpolação / modelagem.

>>> import timeit
>>> def so_q_sub(n):
...  return "%s%s/%d" % (DOMAIN, QUESTIONS, n)
...
>>> so_q_sub(1000)
'http://stackoverflow.com/questions/1000'
>>> def so_q_cat(n):
...  return DOMAIN + QUESTIONS + '/' + str(n)
...
>>> so_q_cat(1000)
'http://stackoverflow.com/questions/1000'
>>> t1 = timeit.Timer('so_q_sub(1000)','from __main__ import so_q_sub')
>>> t2 = timeit.Timer('so_q_cat(1000)','from __main__ import so_q_cat')
>>> t1.timeit(number=10000000)
12.166618871951641
>>> t2.timeit(number=10000000)
5.7813972166853773
>>> t1.timeit(number=1)
1.103492206766532e-05
>>> t2.timeit(number=1)
8.5206360154188587e-06

>>> def so_q_tmp(n):
...  return "{d}{q}/{n}".format(d=DOMAIN,q=QUESTIONS,n=n)
...
>>> so_q_tmp(1000)
'http://stackoverflow.com/questions/1000'
>>> t3= timeit.Timer('so_q_tmp(1000)','from __main__ import so_q_tmp')
>>> t3.timeit(number=10000000)
14.564135316080637

>>> def so_q_join(n):
...  return ''.join([DOMAIN,QUESTIONS,'/',str(n)])
...
>>> so_q_join(1000)
'http://stackoverflow.com/questions/1000'
>>> t4= timeit.Timer('so_q_join(1000)','from __main__ import so_q_join')
>>> t4.timeit(number=10000000)
9.4431309007150048
Vinko Vrsalovic
fonte
10
você fez testes com strings realmente grandes (como 100.000 caracteres)?
drnk
24

Não se esqueça da substituição nomeada:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()
muito php
fonte
4
Este código tem pelo menos 2 práticas de programação ruins: expectativa de variáveis ​​globais (domínio e questões não são declaradas dentro da função) e passar mais variáveis ​​do que o necessário para uma função format (). Downvoting porque esta resposta ensina práticas de codificação ruins.
jperelli
12

Tenha cuidado ao concatenar strings em um loop! O custo da concatenação de strings é proporcional ao comprimento do resultado. O loop leva você direto para a terra do N-quadrado. Algumas linguagens otimizarão a concatenação para a string alocada mais recentemente, mas é arriscado contar com o compilador para otimizar seu algoritmo quadrático até linear. Melhor usar o primitivo ( join?) Que pega uma lista inteira de strings, faz uma única alocação e concatena todas de uma vez.

Norman Ramsey
fonte
16
Isso não é atual. Nas versões mais recentes do python, um buffer de string oculto é criado quando você concatena strings em um loop.
Seun Osewa de
5
@Seun: Sim, como eu disse, algumas linguagens serão otimizadas, mas é uma prática arriscada.
Norman Ramsey
11

"Como a concatenação de strings teve grandes aumentos de desempenho ..."

Se o desempenho for importante, é bom saber.

No entanto, os problemas de desempenho que vi nunca se resumem a operações de string. Geralmente tenho problemas com operações de E / S, classificação e operações O ( n 2 ) sendo os gargalos.

Até que as operações de string sejam os limitadores de desempenho, vou me ater ao que é óbvio. Principalmente, isso é substituição quando é uma linha ou menos, concatenação quando faz sentido e uma ferramenta de modelo (como Mako) quando é grande.

S.Lott
fonte
10

O que você deseja concatenar / interpolar e como deseja formatar o resultado deve orientar sua decisão.

  • A interpolação de strings permite adicionar formatação facilmente. Na verdade, sua versão de interpolação de string não faz a mesma coisa que sua versão de concatenação; na verdade, ele adiciona uma barra extra antes do q_numparâmetro. Para fazer a mesma coisa, você teria que escrever return DOMAIN + QUESTIONS + "/" + str(q_num)nesse exemplo.

  • A interpolação torna mais fácil formatar números; "%d of %d (%2.2f%%)" % (current, total, total/current)seria muito menos legível na forma de concatenação.

  • A concatenação é útil quando você não tem um número fixo de itens para sequenciar.

Além disso, saiba que o Python 2.6 apresenta uma nova versão de interpolação de string, chamada de modelagem de string :

def so_question_uri_template(q_num):
    return "{domain}/{questions}/{num}".format(domain=DOMAIN,
                                               questions=QUESTIONS,
                                               num=q_num)

A modelagem de strings está programada para substituir a% -interpolação, mas isso não acontecerá por um bom tempo, eu acho.

Tim Lesher
fonte
Bem, isso vai acontecer sempre que você decidir mudar para o python 3.0. Além disso, consulte o comentário de Peter para o fato de que você pode fazer substituições nomeadas com o operador% de qualquer maneira.
John Fouhy
"A concatenação é útil quando você não tem um número fixo de itens para encadear." - Você quer dizer uma lista / array? Nesse caso, você não poderia simplesmente juntar-se a eles?
strager
"Você não poderia simplesmente se juntar () a eles?" - Sim (assumindo que você deseja separadores uniformes entre os itens). Compreensões de lista e gerador funcionam muito bem com string.join.
Tim Lesher
1
"Bem, isso vai acontecer sempre que você decidir mudar para python 3.0" - Não, py3k ainda suporta o operador%. O próximo ponto de descontinuação possível é 3.1, então ele ainda tem alguma vida nele.
Tim Lesher
2
2 anos depois ... o python 3.2 está quase sendo lançado e a interpolação de estilo% ainda está bem.
Corey Goldberg
8

Eu estava apenas testando a velocidade de diferentes métodos de concatenação / substituição de strings por curiosidade. Uma pesquisa no google sobre o assunto me trouxe aqui. Pensei em postar os resultados do meu teste na esperança de que ajudasse alguém a decidir.

    import timeit
    def percent_():
            return "test %s, with number %s" % (1,2)

    def format_():
            return "test {}, with number {}".format(1,2)

    def format2_():
            return "test {1}, with number {0}".format(2,1)

    def concat_():
            return "test " + str(1) + ", with number " + str(2)

    def dotimers(func_list):
            # runs a single test for all functions in the list
            for func in func_list:
                    tmr = timeit.Timer(func)
                    res = tmr.timeit()
                    print "test " + func.func_name + ": " + str(res)

    def runtests(func_list, runs=5):
            # runs multiple tests for all functions in the list
            for i in range(runs):
                    print "----------- TEST #" + str(i + 1)
                    dotimers(func_list)

... Depois de executar runtests((percent_, format_, format2_, concat_), runs=5), descobri que o método% era cerca de duas vezes mais rápido que os outros nessas pequenas strings. O método concat sempre foi o mais lento (quase imperceptível). Havia diferenças muito pequenas ao alternar as posições no format()método, mas alternar as posições sempre foi pelo menos 0,01 mais lento do que o método de formato regular.

Amostra de resultados de teste:

    test concat_()  : 0.62  (0.61 to 0.63)
    test format_()  : 0.56  (consistently 0.56)
    test format2_() : 0.58  (0.57 to 0.59)
    test percent_() : 0.34  (0.33 to 0.35)

Eu os executei porque uso concatenação de string em meus scripts e queria saber qual era o custo. Eu os executei em ordens diferentes para ter certeza de que nada estava interferindo ou obtendo um melhor desempenho sendo o primeiro ou o último. Em uma nota lateral, eu adicionei alguns geradores de string mais longos para funções como "%s" + ("a" * 1024)concat regular e foi quase 3 vezes mais rápido (1,1 vs 2,8) do que usar os métodos formate %. Acho que depende das cordas e do que você está tentando alcançar. Se o desempenho realmente importa, talvez seja melhor tentar coisas diferentes e testá-las. Eu tendo a escolher a legibilidade ao invés da velocidade, a menos que a velocidade se torne um problema, mas isso sou só eu. ASSIM não gostei do meu copiar / colar, eu tive que colocar 8 espaços em tudo para que ficasse certo. Eu geralmente uso 4.

Cj Welborn
fonte
1
Você deve considerar seriamente como está traçando o perfil. Por um lado, seu concat é lento porque você tem dois str casts nele. Com strings, o resultado é o oposto, já que string concat é realmente mais rápido do que todas as alternativas quando apenas três strings estão envolvidas.
Justus Wingert
@JustusWingert, agora faz dois anos. Aprendi muito desde que postei este 'teste'. Honestamente, hoje em dia eu uso str.format()e str.join()sobre a concatenação normal. Também estou de olho nos 'f-strings' do PEP 498 , que foi aceito recentemente. Quanto às str()ligações que afetam o desempenho, tenho certeza de que você está certo. Eu não tinha ideia de como as chamadas de função eram caras naquela época. Ainda acho que os testes devem ser feitos quando há alguma dúvida.
Cj Welborn
Depois de um teste rápido com join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), parece que jointambém é mais lento do que a porcentagem.
gaborous
4

Lembre-se de que as decisões estilísticas são decisões práticas, se você planeja manter ou depurar seu código :-) Há uma citação famosa de Knuth (possivelmente citando Hoare?): "Devemos esquecer as pequenas eficiências, digamos cerca de 97% das vezes: Otimização prematura é a raiz de todo o mal."

Contanto que você tome cuidado para não (digamos) transformar uma tarefa O (n) em uma tarefa O (n 2 ), eu faria a que você achar mais fácil de entender.

John Fouhy
fonte
0

Eu uso a substituição sempre que posso. Eu só uso a concatenação se estou construindo uma string em, digamos, um loop for.

Draemon
fonte
7
"construindo uma string em um for-loop" - geralmente é o caso em que você pode usar '' .join e uma expressão geradora.
John Fouhy
-1

Na verdade, a coisa certa a fazer, neste caso (construir caminhos) é usar os.path.join. Não é concatenação de string ou interpolação

Hoskeri
fonte
1
isso é verdade para caminhos de sistema operacional (como em seu sistema de arquivos), mas não ao construir um URI como neste exemplo. URIs sempre têm '/' como separador.
Andre Blum