Який найкращий спосіб об'єднати рядок у 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?


Відповіді:


433

Кращий спосіб додавання рядка в строкової змінної є використання +або +=. Це тому, що воно читабельне та швидке. Вони також настільки ж швидкі, який ви обираєте - це питання смаку, останній - найпоширеніший. Ось таймінги з 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

Ой. Виходить навіть у цьому випадку додавання / з'єднання відбувається повільніше.

То звідки береться ця рекомендація? Python 2?

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

Ну, додавання / приєднання там незначно швидше, якщо ви використовуєте надзвичайно довгі рядки (яких ви зазвичай не є.

Але справжній клінік - Python 2.3. Де я навіть не показуватиму вам терміни, тому що це так повільно, що ще не закінчилося. Ці випробування раптом займають хвилини . За винятком додавання / з'єднання, яке відбувається так само швидко, як і пізніші пітони.

Так. Сполучення струн у Python було дуже повільним ще в кам'яну епоху. Але на 2.4 це вже не є (або принаймні Python 2.4.7), тому рекомендація щодо використання додавання / приєднання застаріла в 2008 році, коли Python 2.3 перестав оновлюватись, і вам слід було б припинити його використання. :-)

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

Однак це CPython. Інші реалізації можуть мати інші проблеми. І це лише ще одна причина, чому передчасна оптимізація є коренем усього зла. Не використовуйте техніку, яка повинна бути "швидшою", якщо спочатку її не виміряєте.

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

То чому я використовую в своєму коді багато додавання / приєднання? Бо іноді насправді зрозуміліше. Особливо, коли все, що ви повинні об'єднати разом, слід розділяти пробілами, комами чи новими рядками.


10
Якщо у вас є кілька рядків (n> 10) "" .join (list_of_strings) все ще швидше
Mikko Ohtamaa

11
Причина, чому + = є швидким, полягає в тому, що в cpython є злом продуктивності, якщо коефіцієнт знижки 1 - він розпадається майже на всі інші реалізації python (за винятком досить спеціальної конфігурованої збірки pypy)
Ronny

17
Чому на це так високо звертаються? Як краще використовувати алгоритм, який є ефективним лише для однієї конкретної реалізації та має те, що по суті становить неміцний злом, щоб виправити квадратичний алгоритм часу? Також ви повністю неправильно розумієте пункт "передчасна оптимізація - корінь усього зла". Ця пропозиція говорить про МАЛІ оптимізації. Це йде від O (n ^ 2) до O (n), що НЕ є малою оптимізацією.
Уес

12
Ось фактична цитата: "Ми повинні забути про малу ефективність, скажімо, про 97% часу: передчасна оптимізація - корінь усього зла. Але ми не повинні передавати свої можливості в ті критичні 3%. Хороший програміст не буде такими міркуваннями приковуються до поступливості, він буде розумним уважно придивитися до критичного коду, але лише після того, як цей код буде визначений "
Уес

2
Ніхто не каже, що а + 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)

Із FAQ на python: який найефективніший спосіб об'єднати багато рядків разом?

Об'єкти str та bytes незмінні, тому об'єднання багатьох рядків разом неефективне, оскільки кожне конкатенація створює новий об'єкт. У загальному випадку загальна вартість виконання є квадратичною у загальній довжині рядка.

Щоб накопичити багато об’єктів str, рекомендованою ідіомою є розміщення їх у списку та виклик str.join () наприкінці:

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

(Ще одна досить ефективна ідіома - використовувати io.StringIO)

Для накопичення багатьох байтових об'єктів рекомендованою ідіомою є розширення об'єкта байтаря, використовуючи на місці конкатенацію (оператор + =):

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

Редагувати: Я був нерозумний, і результати було вставлено назад, і це виглядало так, що додавання до списку було швидше, ніж cStringIO. Я також додав тести на bytearray / str concat, а також другий раунд тестів, використовуючи більший список із більшими рядками. (пітон 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

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

8

Рекомендованим методом залишається використовувати додавання та з'єднання.


1
Як ви бачите з моєї відповіді, це залежить від того, скільки рядків ви об'єднуєте. Я зробив кілька моментів щодо цього (див. Розмову, до якої я посилався у коментарях до своєї відповіді), і, як правило, використовуйте +.
Леннарт Регебро

1
Про це згадує PEP8 ( python.org/dev/peps/pep-0008/#programming-recommendations ). Раціональним є те, що хоча CPython має спеціальні оптимізації для конкатенації рядків з + =, інші реалізації можуть не робити.
Quantum7

8

Якщо рядки, які ви об'єднуєте, є літералами, використовуйте String literal concatenation

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 перешкоджає цьому і заохочує використовувати формат (), приєднатись () та додати () для тривалого використання.

Як цитується з пов'язаного розділу "Рекомендації щодо програмування":

Наприклад, не покладайтесь на ефективну реалізацію CPython об'єднання рядка на місці для висловлювань у вигляді a = = b або a = a + b. Ця оптимізація є крихкою навіть у CPython (вона працює лише для деяких типів) і зовсім не присутня в реалізаціях, які не використовують перерахунок. У чутливих до продуктивності частинах бібліотеки слід використовувати форму '' .join (). Це забезпечить конкатенацію в лінійний час у різних реалізаціях.


5
Посилання було б приємним :)

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

мікро-орієнтир


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

це було порівняно простіше для мене замість використання + чи інших способів


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