Есть ли причина не использовать "+" для объединения двух строк?

124

Распространенным антипаттерном в Python является объединение последовательности строк +в цикле. Это плохо, потому что интерпретатор Python должен создавать новый строковый объект для каждой итерации, а это в конечном итоге занимает квадратичное время. (Последние версии CPython, очевидно, могут оптимизировать это в некоторых случаях, но другие реализации не могут, поэтому программисты не рекомендуется полагаться на это.) ''.join- правильный способ сделать это.

Тем не менее, я слышал (в том числе здесь, в Stack Overflow ), что вы никогда не должны использовать +для конкатенации строк, а вместо этого всегда используйте ''.joinили строку формата. Я не понимаю, почему это так, если вы объединяете только две строки. Если я правильно понимаю, это не должно занимать квадратичное время, и я думаю, что a + bон чище и читабельнее, чем любой ''.join((a, b))или '%s%s' % (a, b).

Является ли хорошей практикой +объединение двух строк? Или есть проблема, о которой я не знаю?

Taymon
источник
Это аккуратнее, и у вас больше контроля, чтобы не выполнять конкатенацию. НО его немного более медленный компромисс с трепом по струнам: P
Jakob Bowyer
Вы говорите +быстрее или медленнее? И почему?
Таймон
1
+ быстрее, In [2]: %timeit "a"*80 + "b"*80 1000000 loops, best of 3: 356 ns per loop In [3]: %timeit "%s%s" % ("a"*80, "b"*80) 1000000 loops, best of 3: 907 ns per loop
Jakob Bowyer
4
In [3]: %timeit "%s%s" % (a, b) 1000000 loops, best of 3: 590 ns per loop In [4]: %timeit a + b 10000000 loops, best of 3: 147 ns per loop
Якоб Бауэр,
1
@JakobBowyer и другие: аргумент «конкатенация строк - это плохо» почти не имеет ничего общего со скоростью, но использует преимущества автоматического преобразования типов с __str__. См. Мой ответ для примеров.
Izkata

Ответы:

120

Нет ничего плохого в том, чтобы объединить две строки с помощью +. На самом деле это легче читать, чем ''.join([a, b]).

Вы правы, хотя объединение более двух строк с помощью +является операцией O (n ^ 2) (по сравнению с O (n) для join) и, таким образом, становится неэффективным. Однако это не связано с использованием цикла. Даже a + b + c + ...O (n ^ 2), причина в том, что каждая конкатенация создает новую строку.

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

ggozad
источник
5
@Mutant: .joinпринимает итерацию, поэтому допустимы оба .join([a,b])и .join((a,b)).
подкидыш
1
Интересные тайминги намекают на использование +или +=в принятом ответе (с 2013 года) на stackoverflow.com/a/12171382/378826 (от Леннарта Регебро) даже для CPython 2.3+ и выбирать шаблон «добавление / присоединение» только в том случае, если этот более ясный Идея решения поставленной проблемы.
Дилетант,
49

Оператор Plus - прекрасное решение для объединения двух строк Python. Но если вы продолжите добавлять более двух строк (n> 25), вы можете подумать о другом.

''.join([a, b, c]) Уловка - это оптимизация производительности.

Микко Охтамаа
источник
2
Разве кортеж не лучше списка?
ThiefMaster
7
Кортеж был бы быстрее - код был просто примером :) Обычно длинные множественные строковые входы являются динамическими.
Микко Охтамаа
5
@martineau Я думаю, он имеет в виду динамическое создание и append()добавление строк в список.
Peter C
5
Здесь нужно сказать: кортеж обычно является МЕДЛЕННОЙ структурой, особенно если он растет. Со списком вы можете использовать list.extend (list_of_items) и list.append (item), которые намного быстрее при динамическом объединении материалов.
Антти Хаапала
6
+1 за n > 25. Людям нужны ориентиры, чтобы с чего-то начать.
n611x007
8

Предположение, что никогда не следует использовать + для конкатенации строк, а вместо этого всегда использовать '.join, может быть мифом. Это правда, что использование +создает ненужные временные копии неизменяемого строкового объекта, но другой не часто цитируемый факт заключается в том, что вызов joinв цикле обычно увеличивает накладные расходы function call. Возьмем ваш пример.

Создайте два списка, один из связанного вопроса SO, а другой - более крупный сфабрикованный

>>> myl1 = ['A','B','C','D','E','F']
>>> myl2=[chr(random.randint(65,90)) for i in range(0,10000)]

Позволяет создать две функции, UseJoinи UsePlusиспользовать соответствующие joinи +функциональность.

>>> def UsePlus():
    return [myl[i] + myl[i + 1] for i in range(0,len(myl), 2)]

>>> def UseJoin():
    [''.join((myl[i],myl[i + 1])) for i in range(0,len(myl), 2)]

Давайте запустим timeit с первым списком

>>> myl=myl1
>>> t1=timeit.Timer("UsePlus()","from __main__ import UsePlus")
>>> t2=timeit.Timer("UseJoin()","from __main__ import UseJoin")
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=100000)/100000)
2.48 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=100000)/100000)
2.61 usec/pass
>>> 

У них почти одинаковая продолжительность работы.

Давайте использовать cProfile

>>> myl=myl2
>>> cProfile.run("UsePlus()")
         5 function calls in 0.001 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <pyshell#1376>:1(UsePlus)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("UseJoin()")
         5005 function calls in 0.029 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.015    0.015    0.029    0.029 <pyshell#1388>:1(UseJoin)
        1    0.000    0.000    0.029    0.029 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     5000    0.014    0.000    0.014    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {range}

И похоже, что использование Join приводит к ненужным вызовам функций, которые могут увеличить накладные расходы.

Теперь вернемся к вопросу. Следует ли препятствовать использованию +сверхjoin во всех случаях ?

Я считаю, что нет, все следует принимать во внимание

  1. Длина рассматриваемой строки
  2. Нет операции конкатенации.

И, конечно же, в разработке преждевременная оптимизация - зло.

Abhijit
источник
7
Конечно, идея заключалась бы не в использовании joinвнутри самого цикла - скорее, цикл генерировал бы последовательность, которая будет передана для соединения.
jsbueno 06
7

При работе с несколькими людьми иногда сложно точно знать, что происходит. Использование строки формата вместо конкатенации может избежать одной неприятности, которая случилась с нами много раз:

Скажем, функции требуется аргумент, и вы пишете его, ожидая получить строку:

In [1]: def foo(zeta):
   ...:     print 'bar: ' + zeta

In [2]: foo('bang')
bar: bang

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

In [3]: foo(23)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

/home/izkata/<ipython console> in foo(zeta)

TypeError: cannot concatenate 'str' and 'int' objects

Не было бы проблем, если бы вы просто использовали строку формата:

In [1]: def foo(zeta):
   ...:     print 'bar: %s' % zeta
   ...:     
   ...:     

In [2]: foo('bang')
bar: bang

In [3]: foo(23)
bar: 23

То же самое верно для всех типов определяющих объектов __str__, которые также могут быть переданы:

In [1]: from datetime import date

In [2]: zeta = date(2012, 4, 15)

In [3]: print 'bar: ' + zeta
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

TypeError: cannot concatenate 'str' and 'datetime.date' objects

In [4]: print 'bar: %s' % zeta
bar: 2012-04-15

Итак, да: если вы можете использовать строку формата, сделайте это и воспользуйтесь тем, что может предложить Python.

Izkata
источник
1
+1 за аргументированное особое мнение. Я все еще думаю, что предпочитаю +.
Таймон
1
Почему бы вам просто не определить метод foo как: print 'bar:' + str (zeta)?
EngineerWithJava54321
@ EngineerWithJava54321 Например, zeta = u"a\xac\u1234\u20ac\U00008000"вам придется использовать, print 'bar: ' + unicode(zeta)чтобы убедиться, что это не ошибка. %sделает это правильно, не задумываясь, и намного короче
Изката
@ EngineerWithJava54321 Другие примеры здесь менее актуальны, но, например, они "bar: %s"могут быть переведены на "zrb: %s br"другой язык. %sВерсия будет только работать, но версия строки-CONCAT станет беспорядком обрабатывать все дела и ваши переводчики теперь будут иметь два отдельных переводов для решения
Izkata
Если они не знают, что такое реализация foo, они столкнутся с этой ошибкой с любым def.
insidesin
3

Я сделал быстрый тест:

import sys

str = e = "a xxxxxxxxxx very xxxxxxxxxx long xxxxxxxxxx string xxxxxxxxxx\n"

for i in range(int(sys.argv[1])):
    str = str + e

и рассчитал это:

mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  8000000
8000000 times

real    0m2.165s
user    0m1.620s
sys     0m0.540s
mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  16000000
16000000 times

real    0m4.360s
user    0m3.480s
sys     0m0.870s

Видимо есть оптимизация под a = a + bслучай. Он не показывает время O (n ^ 2), как можно было бы предположить.

Так что, по крайней мере, с точки зрения производительности, использование +- это нормально.

Майкл Слейд
источник
3
Вы можете сравнить здесь со случаем «присоединиться». И есть вопрос о других реализациях Python, таких как pypy, jython, ironpython и т.д ...
jsbueno
3

Согласно документам Python, использование str.join () обеспечит стабильность производительности в различных реализациях Python. Хотя CPython оптимизирует квадратичное поведение s = s + t, другие реализации Python могут этого не делать.

Детали реализации CPython : если s и t являются строками, некоторые реализации Python, такие как CPython, обычно могут выполнять оптимизацию на месте для назначений формы s = s + t или s + = t. Когда это применимо, эта оптимизация значительно снижает вероятность квадратичного времени выполнения. Эта оптимизация зависит как от версии, так и от реализации. Для кода, чувствительного к производительности, предпочтительно использовать метод str.join (), который обеспечивает согласованную производительность линейной конкатенации между версиями и реализациями.

Типы последовательностей в документации Python (см. Сноску [6])

герцог
источник
2

Я использую следующее с python 3.8

string4 = f'{string1}{string2}{string3}'
Лукас Васкес
источник
0

'' .join ([a, b]) - лучшее решение, чем + .

Поскольку код должен быть написан таким образом, чтобы не ставить в невыгодное положение другие реализации Python (PyPy, Jython, IronPython, Cython, Psyco и т. Д.)

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

https://www.python.org/dev/peps/pep-0008/#programming-recommendations

Мухаммад Али и
источник
1
a += bработает во всех реализациях Python, просто в некоторых из них требуется квадратичное время, когда выполняется внутри цикла ; вопрос касался конкатенации строк вне цикла.
Таймон