Каков наиболее эффективный метод конкатенации строк в python?

148

Есть ли эффективный массовый метод конкатенации строк в Python (например, StringBuilder в C # или StringBuffer в Java)? Я нашел следующие методы здесь :

  • Простое объединение с использованием +
  • Использование списка строк и joinметода
  • Использование UserStringиз MutableStringмодуля
  • Использование массива символов и arrayмодуля
  • Использование cStringIOиз StringIOмодуля

Но что вы, эксперты, используете или предлагаете и почему?

[ Связанный вопрос здесь ]

mshsayem
источник
1
Аналогичный вопрос: stackoverflow.com/questions/476772
Питер Мортенсен
Для объединения известных фрагментов в один, Python 3.6 будет иметь f''строки форматирования, которые будут быстрее любых альтернатив в предыдущих версиях Python.
Антти Хаапала

Ответы:

127

Вы можете быть заинтересованы в этом: Анекдот оптимизации от Guido. Хотя стоит помнить также, что это старая статья, и она предшествует существованию таких вещей, как ''.join(хотя я думаю, string.joinfieldsчто более или менее то же самое)

В силу этого, arrayмодуль может быть самым быстрым, если вы можете включить в него свою проблему. Но ''.join, вероятно, он достаточно быстр и имеет преимущество в том, что он идиоматичен, и поэтому его легче понять другим программистам на Python.

Наконец, золотое правило оптимизации: не оптимизируйте, если вы не знаете, что нужно, и измеряйте, а не угадывайте.

Вы можете измерить различные методы, используя timeitмодуль. Это может сказать вам, что является самым быстрым, вместо случайных незнакомцев в Интернете, делающих догадки.

Джон Фухи
источник
1
Желая добавить к вопросу о том, когда оптимизировать: обязательно проверяйте на худшие случаи. Например, я могу увеличить мой пример, чтобы мой текущий код работал с 0,17 секунды до 170 секунд. Ну, я хочу протестировать на больших размерах выборки, так как там меньше вариаций.
Флиппер
2
«Не оптимизируйте, пока не узнаете, что вам нужно». Если вы просто не используете номинально отличную идиому и можете избежать переделки вашего кода без особых дополнительных усилий.
jeremyjjbrown
1
Одно место, где вы знаете, что вам нужно, это собеседование (это всегда прекрасное время, чтобы освежить ваше глубокое понимание). К сожалению, я не нашел ни одной современной статьи об этом. (1) Java / C # String все еще так плоха в 2017 году? (2) Как насчет C ++? (3) Теперь расскажите о новейших и лучших в Python фокусировках на тех случаях, когда нам нужно сделать миллионы конкатенаций. Можем ли мы верить, что объединение будет работать за линейное время?
user1854182
Что значит «достаточно быстро» .join()? Основной вопрос заключается в том, создает ли это a) копию строки для конкатенации (аналогично s = s + 'abc'), для которой требуется время выполнения O (n), или b) просто добавляется к существующей строке, не создавая копию, для которой требуется O (1) ?
CGFoX
64

''.join(sequenceofstrings) это то, что обычно работает лучше всего - самый простой и быстрый.

Алекс Мартелли
источник
3
@mshsayem, в Python последовательность может быть любым перечисляемым объектом, даже функцией.
Ник Дандулакис
2
Я очень люблю эту ''.join(sequence)идиому. Особенно полезно создавать разделенные запятыми списки: ', '.join([1, 2, 3])выдает строку '1, 2, 3'.
Эндрю Китон
7
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- в этом случае аргумент для присоединения является итератором, созданным с помощью выражения генератора. Там нет временного списка, который создается.
Балфа
2
@balpha: и все же версия генератора медленнее, чем версия для понимания списка: C: \ temp> python -mtimeit "'' .join (chr (x) для x в xrange (65,91))" 100000 циклов, лучший из 3: 9.71 usec на цикл C: \ temp> python -mtimeit "'' .join ([chr (x) для x в xrange (65,91)])" 100000 циклов, лучше всего 3: 7.1 usec на цикл
hughdbrown
1
@hughdbrown, да, когда у вас есть свободная память из списка wazoo (типичный случай), listcomp может быть лучше оптимизирован, чем genexp, часто на 20-30%. Когда в памяти все по-другому - трудно воспроизвести вовремя, хотя! -)
Алекс Мартелли
58

Python 3.6 изменил игру для конкатенации строк известных компонентов с помощью Literal String Interpolation .

Учитывая тестовый пример из ответа Мкойстинена , имея строки

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

Претенденты

  • f'http://{domain}/{lang}/{path}'- 0,151 мкс

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

  • 'http://' + domain + '/' + lang + '/' + path - 0,356 мкс

  • ''.join(('http://', domain, '/', lang, '/', path))- 0,249 мкс (обратите внимание, что построение кортежа постоянной длины немного быстрее, чем создание списка постоянной длины).

Таким образом, в настоящее время самый короткий и самый красивый код также самый быстрый.

В альфа-версиях Python 3.6 реализация f''строк была самой медленной из возможных - фактически сгенерированный байт-код в значительной степени эквивалентен ''.join()случаю с ненужными вызовами, str.__format__которые без аргументов просто возвращали бы selfбез изменений. Эти недостатки были устранены до финальной версии 3.6.

Скорость можно сравнить с самым быстрым методом для Python 2, который является +конкатенацией на моем компьютере; и это занимает 0,203 мкс с 8-битными строками и 0,259 мкс, если все строки являются Unicode.

Антти Хаапала
источник
38

Это зависит от того, что вы делаете.

После Python 2.5 конкатенация строк с оператором + выполняется довольно быстро. Если вы просто объединяете пару значений, лучше использовать оператор +:

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

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

Однако, если вы собираете строку в цикле, вам лучше использовать метод объединения списков:

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

... но обратите внимание, что вы должны собрать относительно большое количество строк, прежде чем разница станет заметной.

Джейсон Бейкер
источник
2
1) В вашем первом измерении, вероятно, на создание списка уходит время. Попробуй с кортежем. 2) CPython работает одинаково хорошо, однако другие реализации Python работают намного хуже с + и + =
u0b34a0f6ae
22

Согласно ответу Джона Фухи, не оптимизируйте без необходимости, но если вы здесь и задаете этот вопрос, это может быть именно потому, что вам нужно . В моем случае мне нужно было собрать несколько URL из строковых переменных ... быстро. Я заметил, что никто (пока), похоже, не рассматривает метод строкового формата, поэтому я подумал, что попробую это, и, в основном, из-за небольшого интереса, я решил добавить туда оператор строковой интерполяции для лучшего измерения. Честно говоря, я не думал, что ни один из них не будет преобразован в прямую операцию «+» или «.join ()». Но угадайте что? В моей системе Python 2.7.5 оператор строковой интерполяции управляет ими всеми, а string.format () работает хуже всех:

# 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), ))

Результаты:

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

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

Теперь, когда у меня был хороший тестовый скрипт, я также тестировал под Python 2.6, 3.3 и 3.4, вот результаты. В Python 2.6 оператор плюс - самый быстрый! На Python 3 присоединение выигрывает. Примечание: эти тесты очень повторяются в моей системе. Таким образом, «плюс» всегда быстрее в 2.6, «intp» всегда быстрее в 2.7 и «соединение» всегда быстрее в 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)

Урок выучен:

  • Иногда мои предположения совершенно неверны.
  • Тест по системе env. вы будете работать в производстве.
  • Строковая интерполяция еще не умерла!

ТЛ; др:

  • Если вы используете 2.6, используйте оператор +.
  • если вы используете 2.7, используйте оператор «%».
  • если вы используете 3.x, используйте '' .join ().
mkoistinen
источник
2
Примечание: интерполяция литеральных строк еще быстрее для 3.6+:f'http://{domain}/{lang}/{path}'
TemporalWolf
1
Кроме того , .format()есть три формы, в порядке от быстрого к медленному: "{}".format(x), "{0}".format(x),"{x}".format(x=x)
TemporalWolf
Реальный урок: когда ваша проблемная область мала, например, составляя короткие строки, метод чаще всего не имеет значения. И даже когда это имеет значение, например, вы действительно строите миллион строк, накладные расходы часто имеют большее значение. Это типичный симптом беспокойства о неправильной проблеме. Только тогда, когда накладные расходы незначительны, например, при создании всей книги в виде строки, разница в методах начинает иметь значение.
Хуэй Чжоу
7

это в значительной степени зависит от относительных размеров новой строки после каждой новой конкатенации. С помощью +оператора для каждой конкатенации создается новая строка. Если промежуточные строки относительно долго, +становится все более и более медленно , потому что новый посредник строка хранится.

Рассмотрим этот случай:

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

Полученные результаты

1 0,00493192672729

2 0.000509023666382

3 0.00042200088501

4 0.000482797622681

В случае 1 и 2 мы добавляем большую строку, а join () работает примерно в 10 раз быстрее. В случаях 3 и 4 мы добавляем небольшую строку, и «+» работает немного быстрее

Дэвид Билен
источник
3

Я столкнулся с ситуацией, когда мне нужно было добавить строку неизвестного размера. Это результаты тестов (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

Это, кажется, показывает, что «+ =» является самым быстрым. Результаты по ссылке Skymind немного устарели.

(Я понимаю, что второй пример не завершен, к окончательному списку нужно будет присоединиться. Однако это показывает, что простая подготовка списка занимает больше времени, чем строка concat.)

MattK
источник
Я получаю саб 1 раз за 3-й и 4-й тесты. Почему вы получаете такие высокие времена? pastebin.com/qabNMCHS
bad_keypoints
@ronnieaka: он получает суб 1 раз за все тесты. Он получает> 1 мкс для 3-го и 4-го, что вы не сделали. Я также получаю более медленное время на этих тестах (на Python 2.7.5, Linux). Может быть процессор, версия, флаги сборки, кто знает.
Танатос
Эти результаты теста бесполезны. В частности, в первом случае, который не выполняет конкатенацию строк, а просто возвращает второе значение строки без изменений.
Антти Хаапала
3

Через год давайте проверим ответ mkoistinen с помощью python 3.4.3:

  • плюс 0,963564149000 (95,83% быстрее)
  • присоединиться 0.923408469000 (100,00% быстрее)
  • Форма 1.501130934000 (скорость 61,51%)
  • int 1.019677452000 (90,56% быстрее)

Ничего не изменилось. Регистрация по-прежнему самый быстрый метод. Поскольку intp является, пожалуй, лучшим выбором с точки зрения читабельности, вы, возможно, захотите использовать intp.

ramsch
источник
1
Возможно, это может быть дополнением к ответу mkoistinen, так как для него не хватает полного ответа (или, по крайней мере, добавьте код, который вы используете).
Триларион
1

Вдохновленный тестами @ JasonBaker, вот простой пример, сравнивающий 10 "abcdefghijklmnopqrstuvxyz"строк и показывающий, что .join()он быстрее; даже с этим крошечным увеличением переменных:

сцепление

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

Присоединиться

>>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
>>> x.timeit()
0.6114138159765048
В
источник
Посмотрите на принятый ответ (прокрутите вниз) на этот вопрос: stackoverflow.com/questions/1349311/…
mshsayem
1

Для небольшого набора из коротких строк (то есть 2 или 3 строк не больше , чем несколько символов), а также по - прежнему намного быстрее. Используя замечательный скрипт mkoistinen в Python 2 и 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)

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

user7505681
источник
1

Вероятно, «новые f-строки в Python 3.6» - наиболее эффективный способ объединения строк.

Используя% s

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

Использование .format

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

Используя f

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

Источник: https://realpython.com/python-f-strings/

SuperNova
источник