Будь-яка причина не використовувати "+" для об'єднання двох рядків?


124

Поширений антипатерн у Python - це об'єднання послідовності рядків, що використовуються +в циклі. Це погано, оскільки інтерпретатору Python для кожної ітерації необхідно створити новий об'єкт рядка, і він закінчується квадратичним часом. (Останні версії CPython, очевидно, можуть оптимізувати це в деяких випадках, але інші реалізації не можуть, тому програмісти не рекомендують покладатися на це.) ''.joinЦе правильний спосіб зробити це.

Однак я чув, як говорилося (в тому числі тут, на переповнюванні стека ), що ви ніколи і ніколи не використовуйте +для конкатенації рядків, а натомість завжди використовуйте ''.joinабо рядок формату. Я не розумію, чому це так, якщо ви об'єднуєте лише два рядки. Якщо моє розуміння правильне, це не повинно зайняти квадратичного часу, і я вважаю a + b, що чистіше і читабельніше, ніж будь-який ''.join((a, b))або '%s%s' % (a, b).

Чи корисно використовувати +для об'єднання двох рядків? Або є проблема, про яку я не знаю?


Його акуратніше, і ви маєте більше контролю, щоб не робити конкатенацію. Але його трохи повільніше, стрибко скачуючи торг: P
Якоб Бойер

Ти кажеш +, швидше чи повільніше? І чому?
Таймон

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
Якоб Боуер

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__. Дивіться мою відповідь для прикладів.
Ізката

Відповіді:


120

Немає нічого поганого в об'єднанні двох рядків з +. Дійсно, це легше читати, ніж ''.join([a, b]).

Ви маєте рацію, хоча об'єднання більш ніж 2 рядків +є операцією O (n ^ 2) (порівняно з O (n) для join) і, таким чином, стає неефективною. Однак це не пов'язане з використанням циклу. Навіть a + b + c + ...є O (n ^ 2), тому причина кожного конкатенації створює нову рядок.

CPython2.4 і вище намагаються пом'якшити це, але все-таки доцільно використовувати joinпри об'єднанні більш ніж 2 рядків.


5
@Mutant: .joinприймає ітерабельний, тому обидва .join([a,b])та .join((a,b))дійсні.
знайдений

1
Цікаві тайминги натяк на використання +або +=в загальноприйнятому відповіді (з 2013 року) на stackoverflow.com/a/12171382/378826 (від Леннарта Regebro) навіть для CPython 2.3+ і до тільки вибрав «Append / приєднатися до " зразком , якщо це ясніше викриває ідея для вирішення проблеми під рукою.
Dilettant

49

Оператор Plus - прекрасне рішення для об'єднання двох рядків Python. Але якщо ви продовжуєте додавати більше двох рядків (n> 25), можливо, ви захочете подумати про щось інше.

''.join([a, b, c]) трюк - оптимізація продуктивності.


2
Невже кортеж не буде кращим за список?
ThiefMaster

7
Кортеж був би швидшим - код був лише прикладом :) Зазвичай довгі введення декількох рядків є динамічними.
Мікко Охтамаа

5
@martineau Я думаю, що він має на увазі динамічне генерування та додавання append()рядків до списку.
Пітер С

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. Ніякої операції з приєднання.

І поза курсом розвитку передзріла оптимізація - це зло.


7
Звичайно, ідея полягала б у тому, щоб не використовувати joinвсередині циклу, а швидше, щоб цикл генерував послідовність, яка буде передана для з'єднання.
jsbueno

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.


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.
всередині

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



0

'' .join ([a, b]) краще рішення, ніж + .

Оскільки код повинен бути написаний таким чином, що не заважає іншим реалізаціям Python (PyPy, Jython, IronPython, Cython, Psyco тощо)

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

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


1
a += bпрацює у всіх реалізаціях Python, просто для деяких з них потрібен квадратичний час, коли це робиться всередині циклу ; питання стосувалося конкатенації рядків поза циклом.
Таймон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.