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

358

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

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

s += stringfromelsewhere

или вот так:

s = []
s.append(somestring)

later

s = ''.join(s)

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

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

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

Максимум
источник
stackoverflow.com/questions/10043636/…
Микко Охтамаа

Ответы:

434

Лучший способ добавления строки в строковой переменной является использование +или +=. Это потому, что это читается и быстро. Они также такие же быстрые, какой из них вы выбираете - дело вкуса, последний является наиболее распространенным. Вот время с 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

Что ж, добавление / объединение происходит немного быстрее, если вы используете очень длинные строки (чего у вас обычно нет, что бы вы имели строку размером 100 МБ в памяти?)

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

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

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

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

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

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

Леннарт Регебро
источник
10
Если у вас есть несколько строк (n> 10), "" .join (list_of_strings) все еще быстрее
Mikko Ohtamaa
11
причина, по которой + = быстро, состоит в том, что в cpython есть хак производительности, если refcount равен 1 - он практически не работает во всех других реализациях python (за исключением довольно специальной сконфигурированной сборки pypy)
Ronny
17
Почему за это так много голосуют? Как лучше использовать алгоритм, который эффективен только в одной конкретной реализации и имеет то, что по сути является хрупким взломом, чтобы исправить алгоритм квадратичного времени? Также вы совершенно неправильно понимаете пункт «преждевременная оптимизация - корень всего зла». Эта цитата говорит о малых оптимизации. Это происходит от O (n ^ 2) до O (n), что НЕ является небольшой оптимизацией.
Уэс
12
Вот фактическая цитата: «Мы должны забыть о малой эффективности, скажем, в 97% случаев: преждевременная оптимизация - корень всего зла. Но мы не должны упускать наши возможности в эти критические 3%. Хороший программист не будет Будучи утешенным такими доводами, он будет мудрым, чтобы внимательно посмотреть на критический код, но только после того, как этот код будет идентифицирован »
Уэс
2
Никто не говорит, что a + b медленный. Это квадратично, когда вы делаете a = a + b более одного раза. a + b + c не медленный, повторяю не медленный, так как он должен проходить каждую строку только один раз, тогда как он должен многократно пересматривать предыдущие строки с подходом a = a + b (при условии, что это в цикле какой-то) Помните, что строки неизменны.
Уэс
52

Если вы объединяете много значений, то ни того, ни другого. Добавление списка стоит дорого. Вы можете использовать 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: Каков наиболее эффективный способ объединения многих строк вместе?

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

Чтобы накапливать много объектов 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
JDI
источник
2
cStringIOне существует в Py3. Используйте io.StringIOвместо этого.
lvc
2
Почему повторное добавление строки может быть дорогим: joelonsoftware.com/articles/fog0000000319.html
Уэс
36

В 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.'
SuperNova
источник
8

Рекомендуемый метод по-прежнему использовать добавление и присоединение.

MRAB
источник
1
Как вы видите из моего ответа, это зависит от того, сколько строк вы объединяете. Я сделал некоторые моменты по этому вопросу (см. Выступление, на которое я ссылался, в моих комментариях к моему ответу), и обычно, если это не больше десяти, используйте +.
Леннарт Регебро
1
PEP8 упоминает об этом ( python.org/dev/peps/pep-0008/#programming-recommendations ). Рациональным является то, что, хотя CPython имеет специальные оптимизации для конкатенации строк с + =, другие реализации не могут.
Quantum7
8

Если объединяемые строки являются литералами, используйте конкатенацию литералов String.

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

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

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

дроид
источник
7

Вы пишете эту функцию

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
Шамим
источник
1
str_join = lambda *str_list: ''.join(s for s in str_list)
Рик поддерживает Монику
7

Использование конкатенации строк вместо '+' является НАИЛУЧШИМ методом конкатенации с точки зрения стабильности и перекрестной реализации, поскольку он не поддерживает все значения. Стандарт PEP8 препятствует этому и поощряет использование format (), join () и append () для долгосрочного использования.

Как указано в связанном разделе «Рекомендации по программированию»:

Например, не полагайтесь на эффективную реализацию CPython конкатенации строк на месте для операторов в форме a + = b или a = a + b. Эта оптимизация хрупка даже в CPython (она работает только для некоторых типов) и не присутствует вообще в реализациях, которые не используют пересчет. В чувствительных к производительности частях библиотеки следует использовать форму '' .join (). Это гарантирует, что конкатенация происходит в линейное время в разных реализациях.

badslacks
источник
6
Ссылка была бы хороша :)
6

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

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

Левон
источник
6

Как упоминает @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()

микро-тест

SAAJ
источник
5

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

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

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

Кушан Гунасекера
источник
3

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

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

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

Ишвар Римал
источник