Який найефективніший метод конкатенації рядків у python?


148

Чи існує якийсь ефективний метод конкатенації рядкових масивів у Python (наприклад, StringBuilder в C # або StringBuffer на Java)? Я знайшов такі методи тут :

  • Просте з’єднання за допомогою +
  • Використання списку рядків і joinметоду
  • Використання UserStringз MutableStringмодуля
  • Використання масиву символів та arrayмодуля
  • Використання cStringIOз StringIOмодуля

Але що ви використовуєте чи пропонуєте фахівці, і чому?

[ Тут пов'язане запитання ]


1
Аналогічне запитання: stackoverflow.com/questions/476772
Пітер Мортенсен

Для об'єднання відомих фрагментів в один, Python 3.6 матиме f''рядки формату, які будуть швидшими, ніж будь-які альтернативи в попередніх версіях Python.
Антті Хаапала

Відповіді:


127

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

В силу цього, arrayмодуль може виявитися найшвидшим, якщо ви зможете підключити свою проблему до нього. Але ''.joinце, мабуть, досить швидко і має перевагу бути ідіоматичним і таким чином легше зрозуміти іншим програмістам python.

Нарешті, золоте правило оптимізації: не оптимізуйте, якщо ви не знаєте, що потрібно, і вимірюйте, а не вгадуючи.

Ви можете вимірювати різні методи за допомогою timeitмодуля. Це може сказати вам, що найшвидше, замість випадкових незнайомих людей в Інтернеті здогадуються.


1
Бажаєте додати питання про те, коли слід оптимізувати: переконайтеся, що перевіряєте найгірші випадки. Наприклад, я можу збільшити зразок, щоб мій поточний код перейшов від 0,17 до 170 секунд. Я хочу перевірити більші розміри зразків, оскільки варіацій там менше.
Фліппер

2
"Не оптимізуйте, поки не знаєте, що вам потрібно." Якщо ви просто не використовуєте номінально іншу фразу і не зможете уникнути переробки коду з невеликими додатковими зусиллями.
jeremyjjbrown

1
Ви знаєте, що вам потрібно пройти співбесіду (це завжди чудовий час, щоб ви зрозуміли своє глибоке розуміння). На жаль, я не знайшов жодної сучасної статті про це. (1) Чи залишається Java / C # String все ще таким поганим у 2017 році? (2) Як щодо C ++? (3) Тепер розповімо про останні та найбільші в Python акценти на випадках, коли нам потрібно робити мільйони конкатенацій. Чи можемо ми вірити, що приєднання буде працювати в лінійний час?
користувач1854182

Що означає «досить швидкий» .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))--- в цьому випадку аргументом приєднання є ітератор, створений за допомогою вираження генератора. Немає тимчасового списку, який будується.
balpha

2
@balpha: і все ж версія генератора повільніше, ніж версія для розуміння списку: C: \ temp> python -mtimeit "" ".join (chr (x) для x in 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 .

З огляду на тестовий випадок з відповіді mkoistinen , що має рядки

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. ви будете працювати у виробництві.
  • Інтерполяція рядків ще не вмерла!

tl; dr:

  • Якщо ви використовуєте 2.6, використовуйте оператор +.
  • якщо ви використовуєте 2.7, використовуйте оператор "%".
  • якщо ви використовуєте 3.x, використовуйте '' .join ().

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 трохи застаріли.

(Я розумію, що другий приклад не є повним. Остаточний список потрібно було б з'єднати. Це, однак, показує, що просто підготовка списку займає більше часу, ніж строковий конмат.)


Я отримую підряд 1 раз на 3-й і 4-й тести. Чому ви отримуєте такі високі часи? pastebin.com/qabNMCHS
bad_keypoints

@ronnieaka: Він отримує пів-секунди рази для всіх тестів. Він отримує> 1 мкс для третього та четвертого, чого ви не зробили. Я також отримую повільніші часи на цих тестах (на Python 2.7.5, Linux). Може бути процесор, версія, збирати прапори, хтозна.
Танатос

Ці результати тестування марні. Тим більше, перший випадок, який не робить жодного з'єднання рядків, просто повертає значення другого рядка неушкодженим.
Антті Хаапала

3

Через рік перевіримо відповідь mkoistinen за допомогою python 3.4.3:

  • плюс 0,963564149000 (95,83% як швидше)
  • приєднайтесь до 0,923408469000 (швидкість 100,00%)
  • форма 1.501130934000 (61,51% як швидше)
  • intp 1.019677452000 (на 90,56% швидше)

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


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)

Тож, коли ваш код робить величезну кількість окремих невеликих конкатенацій, плюс є кращим способом, якщо швидкість має вирішальне значення.


1

Напевно, "нові f-рядки в Python 3.6" є найбільш ефективним способом об'єднання рядків.

Використання% s

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

Використання .формату

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.