Qual é a maneira preferida de concatenar uma string no Python?

358

Como o Python stringnão pode ser alterado, eu queria saber como concatenar uma string com mais eficiência?

Eu posso escrever assim:

s += stringfromelsewhere

ou assim:

s = []
s.append(somestring)

later

s = ''.join(s)

Enquanto escrevia essa pergunta, encontrei um bom artigo falando sobre o tópico.

http://www.skymind.com/~ocrow/python_string/

Mas está no Python 2.x., então a pergunta seria: algo mudou no Python 3?

Máx.
fonte

Respostas:

433

A melhor maneira de acrescentar uma string a uma variável de string é usar +or +=. Isso ocorre porque é legível e rápido. Eles também são rápidos, o que você escolhe é uma questão de gosto, o último é o mais comum. Aqui estão os horários com o timeitmódulo:

a = a + b:
0.11338996887207031
a += b:
0.11040496826171875

No entanto, aqueles que recomendam ter listas e anexá-las e depois ingressar nessas listas, fazem isso porque anexar uma string a uma lista é presumivelmente muito rápido em comparação com a extensão de uma string. E isso pode ser verdade, em alguns casos. Aqui, por exemplo, há um milhão de anexos de uma sequência de um caractere, primeiro a uma sequência e depois a uma lista:

a += b:
0.10780501365661621
a.append(b):
0.1123361587524414

OK, acontece que, mesmo quando a sequência resultante tem um milhão de caracteres, o acréscimo ainda era mais rápido.

Agora vamos tentar anexar uma cadeia de caracteres de mil caracteres centenas de milhares de vezes:

a += b:
0.41823482513427734
a.append(b):
0.010656118392944336

A sequência final, portanto, acaba tendo cerca de 100 MB. Isso foi bem lento, anexar a uma lista era muito mais rápido. Que esse momento não inclua a final a.join(). Então, quanto tempo isso levaria?

a.join(a):
0.43739795684814453

Oups. Acontece que, mesmo nesse caso, a adição / junção é mais lenta.

Então, de onde vem essa recomendação? Python 2?

a += b:
0.165287017822
a.append(b):
0.0132720470428
a.join(a):
0.114929914474

Bem, o acréscimo / junção é marginalmente mais rápido lá se você estiver usando sequências extremamente longas (o que geralmente não é, o que você teria com uma sequência de 100 MB de memória?)

Mas o argumento decisivo é o Python 2.3. Onde eu nem mostrarei os horários, porque é tão lento que ainda não terminou. Esses testes de repente levam minutos . Exceto pelo acréscimo / junção, que é tão rápido quanto nos Pythons posteriores.

Sim. A concatenação de strings era muito lenta no Python na era da pedra. Mas no 2.4 não é mais (ou pelo menos o Python 2.4.7), então a recomendação para usar append / join ficou desatualizada em 2008, quando o Python 2.3 parou de ser atualizado e você deveria ter parado de usá-lo. :-)

(Atualização: Acontece que quando eu fiz o teste com mais cuidado do que usando +e +=é mais rápido para duas seqüências de caracteres no Python 2.3 também. A recomendação de uso ''.join()deve ser um mal-entendido)

No entanto, este é CPython. Outras implementações podem ter outras preocupações. E essa é apenas mais uma razão pela qual a otimização prematura é a raiz de todo mal. Não use uma técnica que seja "mais rápida", a menos que você a avalie primeiro.

Portanto, a versão "melhor" para concatenação de cadeias de caracteres é usar + ou + = . E se isso for lento para você, o que é bastante improvável, faça outra coisa.

Então, por que eu uso muito acréscimo / ingresso no meu código? Porque às vezes é realmente mais claro. Especialmente quando o que você deve concatenar deve ser separado por espaços, vírgulas ou novas linhas.

Lennart Regebro
fonte
10
Se você tiver várias seqüências de caracteres (n> 10) "" .join (list_of_strings) ainda será mais rápido
Mikko Ohtamaa
11
a razão pela qual + = é rápido é que há um corte de desempenho no cpython se o refcount for 1 - ele se desfaz de praticamente todas as outras implementações de python (com exceção de uma compilação de pypy configurada bastante especial)
Ronny
17
Por que isso está sendo tão votado? Como é melhor usar um algoritmo que seja eficiente apenas em uma implementação específica e que tenha essencialmente um hack frágil para corrigir um algoritmo de tempo quadrático? Além disso, você entende completamente errado o ponto de "otimização prematura é a raiz de todo mal". Essa citação está falando sobre pequenas otimizações. Isso vai de O (n ^ 2) a O (n) que NÃO é uma otimização pequena.
Wes
12
Aqui está a citação real: "Devemos esquecer pequenas eficiências, digamos, cerca de 97% das vezes: a otimização prematura é a raiz de todo mal. No entanto, não devemos desperdiçar nossas oportunidades nesses 3% críticos. Um bom programador não ser embalado em complacência por esse raciocínio, ele será sensato em examinar atentamente o código crítico; mas somente após esse código ter sido identificado "
Wes
2
Ninguém está dizendo que a + b é lento. É quadrático quando você está fazendo a = a + b mais de uma vez. a + b + c não é lento, repito não lento, pois ele só precisa percorrer cada corda uma vez, enquanto que deve percorrer as cadeias anteriores muitas vezes com a abordagem a = a + b (assumindo que esteja em loop De algum tipo). Lembre-se de que as cordas são imutáveis.
Wes
52

Se você está concatenando muitos valores, nenhum deles. Anexar uma lista é caro. Você pode usar o StringIO para isso. Especialmente se você estiver desenvolvendo várias operações.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

buf.write('foo')
buf.write('foo')
buf.write('foo')

buf.getvalue()
# 'foofoofoo'

Se você já tiver uma lista completa retornada de outra operação, basta usar o ''.join(aList)

Da FAQ do python: Qual é a maneira mais eficiente de concatenar muitas strings juntas?

Os objetos str e bytes são imutáveis; portanto, concatenar muitas strings juntas é ineficiente, pois cada concatenação cria um novo objeto. No caso geral, o custo total do tempo de execução é quadrático no comprimento total da cadeia.

Para acumular muitos objetos str, o idioma recomendado é colocá-los em uma lista e chamar str.join () no final:

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(outro idioma razoavelmente eficiente é usar io.StringIO)

Para acumular muitos objetos de bytes, o idioma recomendado é estender um objeto de bytearray usando a concatenação no local (o operador + =):

result = bytearray()
for b in my_bytes_objects:
    result += b

Edit: Eu era bobo e tinha os resultados colados para trás, fazendo parecer que anexar a uma lista era mais rápido que o cStringIO. Também adicionei testes para bytearray / str concat, bem como uma segunda rodada de testes usando uma lista maior com cadeias maiores. (python 2.7.3)

exemplo de teste ipython para grandes listas de strings

try:
    from cStringIO import StringIO
except:
    from io import StringIO

source = ['foo']*1000

%%timeit buf = StringIO()
for i in source:
    buf.write(i)
final = buf.getvalue()
# 1000 loops, best of 3: 1.27 ms per loop

%%timeit out = []
for i in source:
    out.append(i)
final = ''.join(out)
# 1000 loops, best of 3: 9.89 ms per loop

%%timeit out = bytearray()
for i in source:
    out += i
# 10000 loops, best of 3: 98.5 µs per loop

%%timeit out = ""
for i in source:
    out += i
# 10000 loops, best of 3: 161 µs per loop

## Repeat the tests with a larger list, containing
## strings that are bigger than the small string caching 
## done by the Python
source = ['foo']*1000

# cStringIO
# 10 loops, best of 3: 19.2 ms per loop

# list append and join
# 100 loops, best of 3: 144 ms per loop

# bytearray() +=
# 100 loops, best of 3: 3.8 ms per loop

# str() +=
# 100 loops, best of 3: 5.11 ms per loop
jdi
fonte
2
cStringIOnão existe no Py3. Use em io.StringIOvez disso.
Lvc
2
Quanto ao porquê acrescentar uma string repetidamente pode ser caro: joelonsoftware.com/articles/fog0000000319.html
Wes
36

No Python> = 3.6, a nova string f é uma maneira eficiente de concatenar uma string.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'
Super Nova
fonte
8

O método recomendado ainda é usar anexar e ingressar.

MRAB
fonte
11
Como você vê na minha resposta, isso depende de quantas seqüências você está concatenando. Eu fiz alguns horários para isso (veja a palestra à qual vinculei meus comentários na minha resposta) e, geralmente, a menos que sejam mais de dez, use +.
Lennart Regebro
11
O PEP8 menciona isso ( python.org/dev/peps/pep-0008/#programming-recommendations ). O racional é que, enquanto o CPython possui otimizações especiais para concatenação de cadeias com + =, outras implementações podem não ter.
precisa saber é o seguinte
8

Se as seqüências de caracteres que você está concatenando forem literais, use concatenação literal de String

re.compile(
        "[A-Za-z_]"       # letter or underscore
        "[A-Za-z0-9_]*"   # letter, digit or underscore
    )

Isso é útil se você quiser comentar parte de uma string (como acima) ou se desejar usar strings brutas ou aspas triplas para parte de um literal, mas não todos.

Como isso acontece na camada de sintaxe, ele usa zero operadores de concatenação.

andróide
fonte
7

Você escreve esta função

def str_join(*args):
    return ''.join(map(str, args))

Então você pode ligar simplesmente para onde quiser

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3
Shameem
fonte
11
str_join = lambda *str_list: ''.join(s for s in str_list)
Rick suporta Monica
7

Usar concatenação de string no local por '+' é o pior método de concatenação em termos de estabilidade e implementação cruzada, pois não suporta todos os valores. O padrão PEP8 desencoraja isso e incentiva o uso de format (), join () e append () para uso a longo prazo.

Conforme citado na seção "Recomendações de programação" vinculada:

Por exemplo, não conte com a implementação eficiente do CPython de concatenação de cadeias no local para instruções no formato a + = b ou a = a + b. Essa otimização é frágil mesmo no CPython (funciona apenas para alguns tipos) e não está presente em implementações que não usam refcounting. Em partes sensíveis ao desempenho da biblioteca, o formulário '' .join () deve ser usado. Isso garantirá que a concatenação ocorra em tempo linear em várias implementações.

badslacks
fonte
5
Link de referência teria sido bom :)
6

Embora um pouco datado, código gosta de uma Pythonista: Idiomatic Python recomenda join()ao longo + desta seção . Assim como PythonSpeedPerformanceTips em sua seção sobre concatenação de strings , com o seguinte aviso:

A precisão desta seção é contestada em relação às versões posteriores do Python. No CPython 2.5, a concatenação de strings é bastante rápida, embora isso possa não se aplicar da mesma forma a outras implementações do Python. Consulte ConcatenationTestCode para uma discussão.

Levon
fonte
6

Como o @jdi menciona, a documentação do Python sugere usar str.joinou io.StringIOpara concatenação de strings. E diz que um desenvolvedor deve esperar um tempo quadrático +=em um loop, mesmo que haja uma otimização desde o Python 2.4. Como esta resposta diz:

Se o Python detectar que o argumento esquerdo não tem outras referências, ele realloctentará evitar uma cópia redimensionando a string no lugar. Isso não é algo em que você deve confiar, porque é um detalhe de implementação e, se reallocacabar precisando mover a string com frequência, o desempenho é degradado para O (n ^ 2) de qualquer maneira.

Vou mostrar um exemplo de código do mundo real que ingenuamente se baseou +=nessa otimização, mas não se aplicou. O código abaixo converte uma sequência iterável de sequências curtas em partes maiores para serem usadas em uma API em massa.

def test_concat_chunk(seq, split_by):
    result = ['']
    for item in seq:
        if len(result[-1]) + len(item) > split_by: 
            result.append('')
        result[-1] += item
    return result

Esse código pode funcionar literariamente por horas devido à complexidade do tempo quadrático. Abaixo estão alternativas com estruturas de dados sugeridas:

import io

def test_stringio_chunk(seq, split_by):
    def chunk():
        buf = io.StringIO()
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                size += buf.write(item)
            else:
                yield buf.getvalue()
                buf = io.StringIO()
                size = buf.write(item)
        if size:
            yield buf.getvalue()

    return list(chunk())

def test_join_chunk(seq, split_by):
    def chunk():
        buf = []
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                buf.append(item)
                size += len(item)
            else:
                yield ''.join(buf)                
                buf.clear()
                buf.append(item)
                size = len(item)
        if size:
            yield ''.join(buf)

    return list(chunk())

E uma micro-referência:

import timeit
import random
import string
import matplotlib.pyplot as plt

line = ''.join(random.choices(
    string.ascii_uppercase + string.digits, k=512)) + '\n'
x = []
y_concat = []
y_stringio = []
y_join = []
n = 5
for i in range(1, 11):
    x.append(i)
    seq = [line] * (20 * 2 ** 20 // len(line))
    chunk_size = i * 2 ** 20
    y_concat.append(
        timeit.timeit(lambda: test_concat_chunk(seq, chunk_size), number=n) / n)
    y_stringio.append(
        timeit.timeit(lambda: test_stringio_chunk(seq, chunk_size), number=n) / n)
    y_join.append(
        timeit.timeit(lambda: test_join_chunk(seq, chunk_size), number=n) / n)
plt.plot(x, y_concat)
plt.plot(x, y_stringio)
plt.plot(x, y_join)
plt.legend(['concat', 'stringio', 'join'], loc='upper left')
plt.show()

micro-benchmark

saaj
fonte
5

Você pode fazer de maneiras diferentes.

str1 = "Hello"
str2 = "World"
str_list = ['Hello', 'World']
str_dict = {'str1': 'Hello', 'str2': 'World'}

# Concatenating With the + Operator
print(str1 + ' ' + str2)  # Hello World

# String Formatting with the % Operator
print("%s %s" % (str1, str2))  # Hello World

# String Formatting with the { } Operators with str.format()
print("{}{}".format(str1, str2))  # Hello World
print("{0}{1}".format(str1, str2))  # Hello World
print("{str1} {str2}".format(str1=str_dict['str1'], str2=str_dict['str2']))  # Hello World
print("{str1} {str2}".format(**str_dict))  # Hello World

# Going From a List to a String in Python With .join()
print(' '.join(str_list))  # Hello World

# Python f'strings --> 3.6 onwards
print(f"{str1} {str2}")  # Hello World

Eu criei este pequeno resumo através dos seguintes artigos.

Kushan Gunasekera
fonte
3

meu caso de uso foi ligeiramente diferente. Eu tive que construir uma consulta em que mais de 20 campos fossem dinâmicos. Eu segui essa abordagem de usar o método format

query = "insert into {0}({1},{2},{3}) values({4}, {5}, {6})"
query.format('users','name','age','dna','suzan',1010,'nda')

isso foi comparativamente mais simples para mim, em vez de usar + ou de outras maneiras

Ishwar Rimal
fonte