Ответ 1
Речь идет не об использовании inplace +=
versus +
binary add. Вы не рассказали нам всю историю. Ваша исходная версия объединила 3 строки, а не только две:
sTable = sTable + '\n' + sRow # simplified, sRow is a function call
Python пытается помочь и оптимизировать конкатенацию строк; как при использовании strobj += otherstrobj
, так и strobj = strobj + otherstringobj
, но он не может применить эту оптимизацию, когда задействовано более 2 строк.
Строки Python являются неизменяемыми обычно, но если нет других ссылок на объект с левой строкой и он все равно отскакивается, Python обманывает и изменяет строку. Это позволяет избежать необходимости создавать новую строку при каждом объединении, что может привести к значительному улучшению скорости.
Это реализовано в цикле оценки байт-кода. И при использовании BINARY_ADD
для двух строк, и при использовании INPLACE_ADD
для двух строк, Python делегирует конкатенацию к специальной вспомогательной функции string_concatenate()
. Чтобы иметь возможность оптимизировать конкатенацию, изменяя строку, сначала нужно убедиться, что в строке нет других ссылок на нее; если только стек и исходная переменная ссылаются на это, это может быть сделано, и операция next заменит исходную ссылку на переменную.
Итак, если на строку есть всего 2 ссылки, а следующий оператор - один из STORE_FAST
(установите локальную переменную), STORE_DEREF
(установите переменную, на которую ссылаются закрытые функции) или STORE_NAME
(задано глобальная переменная), а затронутая переменная в настоящее время ссылается на одну и ту же строку, тогда эта целевая переменная очищается, чтобы уменьшить количество ссылок только на 1, стек.
И вот почему ваш исходный код не смог полностью использовать эту оптимизацию. Первая часть вашего выражения - sTable + '\n'
, а следующая операция - другая BINARY_ADD
:
>>> import dis
>>> dis.dis(compile(r"sTable = sTable + '\n' + sRow", '<stdin>', 'exec'))
1 0 LOAD_NAME 0 (sTable)
3 LOAD_CONST 0 ('\n')
6 BINARY_ADD
7 LOAD_NAME 1 (sRow)
10 BINARY_ADD
11 STORE_NAME 0 (sTable)
14 LOAD_CONST 1 (None)
17 RETURN_VALUE
За первым BINARY_ADD
следует LOAD_NAME
для доступа к переменной sRow
, а не к операции хранения. Этот первый BINARY_ADD
должен всегда приводить к появлению нового строкового объекта, когда все больше sTable
растет, и для создания этого нового строкового объекта требуется больше времени.
Вы изменили этот код на:
sTable += '\n%s' % sRow
который удалил вторую конкатенацию. Теперь байт-код:
>>> dis.dis(compile(r"sTable += '\n%s' % sRow", '<stdin>', 'exec'))
1 0 LOAD_NAME 0 (sTable)
3 LOAD_CONST 0 ('\n%s')
6 LOAD_NAME 1 (sRow)
9 BINARY_MODULO
10 INPLACE_ADD
11 STORE_NAME 0 (sTable)
14 LOAD_CONST 1 (None)
17 RETURN_VALUE
и все, что у нас осталось, - это INPLACE_ADD
, за которым следует магазин. Теперь sTable
может быть изменен на месте, не приводя к появлению более крупного нового строкового объекта.
У вас была бы такая же разница в скорости:
sTable = sTable + ('\n%s' % sRow)
здесь.
Временное испытание показывает разницу:
>>> import random
>>> from timeit import timeit
>>> testlist = [''.join([chr(random.randint(48, 127)) for _ in range(random.randrange(10, 30))]) for _ in range(1000)]
>>> def str_threevalue_concat(lst):
... res = ''
... for elem in lst:
... res = res + '\n' + elem
...
>>> def str_twovalue_concat(lst):
... res = ''
... for elem in lst:
... res = res + ('\n%s' % elem)
...
>>> timeit('f(l)', 'from __main__ import testlist as l, str_threevalue_concat as f', number=10000)
6.196403980255127
>>> timeit('f(l)', 'from __main__ import testlist as l, str_twovalue_concat as f', number=10000)
2.3599119186401367
Мораль этой истории заключается в том, что вы не должны использовать конкатенацию строк в первую очередь. Правильный способ создания новой строки из множества других строк - использовать список, а затем использовать str.join()
:
table_rows = []
for something in something_else:
table_rows += ['\n', GetRow()]
sTable = ''.join(table_rows)
Это быстрее:
>>> def str_join_concat(lst):
... res = ''.join(['\n%s' % elem for elem in lst])
...
>>> timeit('f(l)', 'from __main__ import testlist as l, str_join_concat as f', number=10000)
1.7978830337524414
но вы не можете использовать только '\n'.join(lst)
:
>>> timeit('f(l)', 'from __main__ import testlist as l, nl_join_concat as f', number=10000)
0.23735499382019043