Каков предпочтительный способ конкатенации строки в Python?

Поскольку Python string не может быть изменен, мне было интересно, как более эффективно конкатенировать строку?

Я могу написать вот так:

s += stringfromelsewhere

или вот так:

s = []
s.append(somestring)

later

s = ''.join(s)

При написании этого вопроса я нашел хорошую статью, рассказывающую о теме.

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

Но это в Python 2.x., так что вопрос будет что-то менять в Python 3?

Ответ 1

Лучший способ добавления строки к строковой переменной - использовать + или +=. Это потому, что оно читаемо и быстро. Они также так же быстро, что вы выбираете, это вопрос вкуса, последний из них наиболее распространен. Ниже приведены тайминги с модулем timeit:

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

Однако те, кто рекомендует иметь списки и добавлять к ним, а затем присоединяются к этим спискам, делают это, потому что добавление строки в список, по-видимому, очень быстро по сравнению с расширением строки. И это может быть правдой, в некоторых случаях. Вот, например, один миллион добавляет односимвольную строку, сначала в строку, затем в список:

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

ОК, получается, что даже когда результирующая строка составляет миллион символов, добавление еще быстрее.

Теперь попробуйте с добавлением строки длиной в тысячу символов сто тысяч раз:

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

Конечная строка, следовательно, заканчивается длиной около 100 МБ. Это было довольно медленно, добавление к списку было намного быстрее. Это время не включает окончательный a.join(). Так как долго это займет?

a.join(a):
0.43739795684814453

Oups. Выключается даже в этом случае, добавление/соединение происходит медленнее.

Итак, откуда эта рекомендация? Python 2?

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

Ну, append/join там немного быстрее, если вы используете чрезвычайно длинные строки (чего вы обычно не знаете, что бы у вас была строка, в которой 100 МБ в памяти?)

Но реальным clincher является Python 2.3. Где я даже не покажу вам тайминги, потому что он настолько медленный, что он еще не закончил. Эти тесты внезапно занимают минуты. За исключением append/join, что так же быстро, как и при последующих Python.

Угу. Конкатенация строк была очень медленной в Python в каменном веке. Но на 2.4 это уже не (или, по крайней мере, Python 2.4.7), поэтому рекомендация использовать append/join устарела в 2008 году, когда Python 2.3 перестает быть обновленным, и вы должны прекратить ее использовать.: -)

(Обновление: Оказывается, когда я тщательно тестировал, что использование + и += выполняется быстрее для двух строк на Python 2.3. Рекомендация использовать ''.join() должна быть недоразумением)

Однако это CPython. Другие реализации могут иметь другие проблемы. И это еще одна причина, почему преждевременная оптимизация - это корень всего зла. Не используйте технику, которая предположительно "быстрее", если вы ее не измерили.

Поэтому "лучшей" версией для выполнения конкатенации строк является использование + или + =. И если это окажется для вас медленным, что маловероятно, тогда сделайте что-нибудь еще.

Так почему я использую много append/join в моем коде? Потому что иногда это на самом деле яснее. Особенно, когда все, что вы должны объединить вместе, должно быть разделено пробелами или запятыми или символами новой строки.

Ответ 2

Если вы объединяете множество значений, то ни то, ни другое. Добавление списка дорого. Вы можете использовать StringIO для этого. Особенно, если вы строите его на протяжении многих операций.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

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

buf.getvalue()
# 'foofoofoo'

Если у вас уже есть полный список, возвращенный вам из какой-либо другой операции, просто используйте ''.join(aList)

Из python FAQ: Каков наиболее эффективный способ объединить многие строки вместе?

объекты str и байты неизменяемы, поэтому объединение многих строки вместе неэффективны, поскольку каждая конкатенация создает новый объект. В общем случае общая стоимость времени выполнения является квадратичной в общая длина строки.

Чтобы скопировать много объектов str, рекомендуемая идиома - разместить их в список и вызовите str.join() в конце:

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

(еще одна разумно эффективная идиома - использовать io.StringIO)

Чтобы накопить много объектов с байтами, рекомендуемая идиома состоит в том, чтобы расширить bytearray с использованием конкатенации на месте (оператор + =):

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

Изменить: я был глупым, и результаты были вставлены в обратном направлении, поэтому похоже, что добавление к списку было быстрее, чем cStringIO. Я также добавил тесты для bytearray/str concat, а также второй раунд тестов, используя более крупный список с большими строками. (python 2.7.3)

Пример теста ipython для больших списков строк

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

Ответ 3

В Python> = 3.6 новая f-строка является эффективным способом объединения строк.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'

Ответ 4

Рекомендуемый метод все еще должен использовать append и join.

Ответ 5

Если строки, которые вы конкатенируете, являются литералами, используйте Конкатенация строк строки

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

Это полезно, если вы хотите прокомментировать часть строки (как указано выше) или если вы хотите использовать необработанные строки или тройные кавычки для части литерала но не все.

Так как это происходит на уровне синтаксиса, он использует нулевые операторы конкатенации.

Ответ 6

Использование конкатенации строк на месте с помощью '+' - это метод НАСТРОЙКИ КОНЕЧНОСТИ с точки зрения стабильности и кросс-реализации, поскольку он не поддерживает все значения. Стандарт PEP8 обескураживает это и поощряет использование format(), join() и append() для долгосрочного использования.

Ответ 7

В то время как несколько устарел, Код Как и Pythonista: Идиоматический Python рекомендует join() over + в этом разделе. Как PythonSpeedPerformanceTips в разделе конкатенация строк, с после отказа от ответственности:

Точность этого раздела оспаривается в отношении более позднего версии Python. В CPython 2.5 конкатенация строк довольно быстро, хотя это может не относиться также к другому Python Реализации. См. ConcatenationTestCode для обсуждения.

Ответ 8

Вы записываете эту функцию

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

Затем вы можете звонить просто там, где хотите

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3

Ответ 9

Как упоминает @jdi, документация Python предлагает использовать str.join или io.StringIO для конкатенации строк. И говорит, что разработчик должен ожидать квадратичного времени от += в цикле, даже несмотря на оптимизацию, начиная с Python 2.4. Как этот ответ говорит:

Если Python обнаруживает, что левый аргумент не имеет других ссылок, он вызывает realloc чтобы попытаться избежать копирования, изменив размер строки на месте. Это не то, на что вам следует когда-либо полагаться, потому что это деталь реализации и потому, что если realloc конечном итоге придется часто перемещать строку, производительность в любом случае снижается до O (n ^ 2).

Я покажу пример реального кода, который наивно полагался на += эту оптимизацию, но она не применялась. Приведенный ниже код преобразует итерируемые короткие строки в большие куски для использования в массовом API.

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

Этот код может работать в течение нескольких часов из-за квадратичной сложности времени. Ниже приведены альтернативы с предлагаемыми структурами данных:

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())

И микро-тест:

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

Ответ 10

Мой случай использования был немного другим. Мне пришлось построить запрос, где более 20 полей были динамическими. Я придерживался этого подхода использования метода форматирования

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

это было сравнительно проще для меня вместо использования + или других способов

Ответ 12

Вы можете сделать по-разному.

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

Я создал это небольшое резюме в следующих статьях.