Конкатенация строк против подстановки строк в Python

98

В Python меня ускользает от того, где и когда использовать конкатенацию строк вместо подстановки строк. Поскольку конкатенация строк значительно повысила производительность, является ли это (становится все более) стилистическим решением, а не практическим?

В качестве конкретного примера, как следует обрабатывать создание гибких URI:

DOMAIN = 'http://stackoverflow.com'
QUESTIONS = '/questions'

def so_question_uri_sub(q_num):
    return "%s%s/%d" % (DOMAIN, QUESTIONS, q_num)

def so_question_uri_cat(q_num):
    return DOMAIN + QUESTIONS + '/' + str(q_num)

Изменить: также были предложения о присоединении к списку строк и использовании именованной замены. Это варианты по центральной теме, а именно: как правильно это делать и в какое время? Спасибо за ответы!

gotgenes
источник
Забавно, но в Ruby интерполяция строк обычно быстрее, чем конкатенация ...
Келтия
вы забыли вернуть "" .join ([DOMAIN, QUESTIONS, str (q_num)])
Джимми
Я не эксперт по Ruby, но готов поспорить, что интерполяция быстрее, потому что строки в Ruby изменяемы. Строки в Python - это неизменяемые последовательности.
gotgenes
1
небольшой комментарий по поводу URI. URI не совсем похожи на строки. Существуют URI, поэтому вы должны быть очень осторожны при их объединении или сравнении. Пример: сервер, доставляющий свои представления через http на порт 80. example.org (без слэша в конце) example.org/ (слэш) example.org:80/ (слэш + порт 80) - это тот же uri, но не одинаковый строка.
karlcow 05

Ответы:

55

Согласно моей машине, конкатенация (значительно) быстрее. Но стилистически я готов заплатить цену замены, если производительность не критична. Ну, а если мне нужно форматирование, не нужно даже задавать вопрос ... нет другого выхода, кроме как использовать интерполяцию / шаблон.

>>> import timeit
>>> def so_q_sub(n):
...  return "%s%s/%d" % (DOMAIN, QUESTIONS, n)
...
>>> so_q_sub(1000)
'http://stackoverflow.com/questions/1000'
>>> def so_q_cat(n):
...  return DOMAIN + QUESTIONS + '/' + str(n)
...
>>> so_q_cat(1000)
'http://stackoverflow.com/questions/1000'
>>> t1 = timeit.Timer('so_q_sub(1000)','from __main__ import so_q_sub')
>>> t2 = timeit.Timer('so_q_cat(1000)','from __main__ import so_q_cat')
>>> t1.timeit(number=10000000)
12.166618871951641
>>> t2.timeit(number=10000000)
5.7813972166853773
>>> t1.timeit(number=1)
1.103492206766532e-05
>>> t2.timeit(number=1)
8.5206360154188587e-06

>>> def so_q_tmp(n):
...  return "{d}{q}/{n}".format(d=DOMAIN,q=QUESTIONS,n=n)
...
>>> so_q_tmp(1000)
'http://stackoverflow.com/questions/1000'
>>> t3= timeit.Timer('so_q_tmp(1000)','from __main__ import so_q_tmp')
>>> t3.timeit(number=10000000)
14.564135316080637

>>> def so_q_join(n):
...  return ''.join([DOMAIN,QUESTIONS,'/',str(n)])
...
>>> so_q_join(1000)
'http://stackoverflow.com/questions/1000'
>>> t4= timeit.Timer('so_q_join(1000)','from __main__ import so_q_join')
>>> t4.timeit(number=10000000)
9.4431309007150048
Винко Врсалович
источник
10
вы делали тесты с настоящими большими строками (например, 100000 символов)?
drnk
24

Не забываем про именованную замену:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()
слишком много php
источник
4
В этом коде есть как минимум две плохие практики программирования: ожидание глобальных переменных (домен и вопросы не объявляются внутри функции) и передача большего количества переменных, чем необходимо в функцию format (). Голосование против, потому что этот ответ учит плохим методам программирования.
jperelli 06
12

Остерегайтесь объединения строк в цикл! Стоимость конкатенации строк пропорциональна длине результата. Зацикливание приведет вас прямо в страну N-квадрата. Некоторые языки оптимизируют конкатенацию с последней выделенной строкой, но опасно рассчитывать на то, что компилятор оптимизирует ваш квадратичный алгоритм до линейного. Лучше всего использовать примитив ( join?), Который принимает весь список строк, выполняет одно распределение и объединяет их все за один раз.

Норман Рэмси
источник
16
Это не актуально. В последних версиях Python при объединении строк в цикл создается скрытый строковый буфер.
Сеун Осева,
5
@Seun: Да, как я уже сказал, некоторые языки оптимизируются, но это рискованная практика.
Норман Рэмси,
11

«Поскольку конкатенация строк значительно повысила производительность ...»

Если производительность имеет значение, это полезно знать.

Однако проблемы с производительностью, которые я видел, никогда не сводились к строковым операциям. У меня обычно были проблемы с вводом-выводом, сортировкой и O ( n 2 операциями ), которые были узкими местами.

До тех пор, пока строковые операции не станут ограничителями производительности, я буду придерживаться очевидных вещей. В основном это подстановка, когда это одна строка или меньше, конкатенация, когда это имеет смысл, и инструмент шаблона (например, Mako), когда он большой.

С.Лотт
источник
10

Что вы хотите объединить / интерполировать и как вы хотите отформатировать результат, должно определять ваше решение.

  • Интерполяция строк позволяет легко добавлять форматирование. Фактически, ваша версия с интерполяцией строк не делает то же самое, что и версия с конкатенацией; он фактически добавляет перед q_numпараметром дополнительную косую черту . Чтобы сделать то же самое, вам нужно будет написать return DOMAIN + QUESTIONS + "/" + str(q_num)в этом примере.

  • Интерполяция упрощает форматирование чисел; "%d of %d (%2.2f%%)" % (current, total, total/current)будет гораздо менее читаемым в форме конкатенации.

  • Конкатенация полезна, когда у вас нет фиксированного количества элементов для преобразования в строку.

Также знайте, что Python 2.6 представляет новую версию строковой интерполяции, которая называется строковым шаблоном :

def so_question_uri_template(q_num):
    return "{domain}/{questions}/{num}".format(domain=DOMAIN,
                                               questions=QUESTIONS,
                                               num=q_num)

Планируется, что шаблонирование строк в конечном итоге заменит% -интерполяцию, но я думаю, что это произойдет не скоро.

Тим Лешер
источник
Что ж, это произойдет всякий раз, когда вы решите перейти на Python 3.0. Также см. Комментарий Питера о том, что вы в любом случае можете выполнять именованные замены с помощью оператора%.
Джон Фухи
«Конкатенация полезна, когда у вас нет фиксированного количества элементов для преобразования в строку». - Вы имеете в виду список / массив? В таком случае, не могли бы вы просто присоединиться () к ним?
Strager
"Не могли бы вы просто присоединиться () к ним?" - Да (при условии, что вам нужны единые разделители между элементами). Понимание списков и генераторов отлично работает с string.join.
Тим Лешер
1
«Что ж, это произойдет всякий раз, когда вы решите перейти на python 3.0» - Нет, py3k по-прежнему поддерживает оператор%. Следующая возможная точка устаревания - 3.1, так что в ней еще есть жизнь.
Тим Лешер
2
2 года спустя ... python 3.2 приближается к выпуску, а интерполяция стиля% все еще в порядке.
Кори Голдберг,
8

Я просто из любопытства тестировал скорость различных методов конкатенации / подстановки строк. Поиск в Google по этой теме привел меня сюда. Я думал, что опубликую результаты своих тестов в надежде, что они помогут кому-нибудь принять решение.

    import timeit
    def percent_():
            return "test %s, with number %s" % (1,2)

    def format_():
            return "test {}, with number {}".format(1,2)

    def format2_():
            return "test {1}, with number {0}".format(2,1)

    def concat_():
            return "test " + str(1) + ", with number " + str(2)

    def dotimers(func_list):
            # runs a single test for all functions in the list
            for func in func_list:
                    tmr = timeit.Timer(func)
                    res = tmr.timeit()
                    print "test " + func.func_name + ": " + str(res)

    def runtests(func_list, runs=5):
            # runs multiple tests for all functions in the list
            for i in range(runs):
                    print "----------- TEST #" + str(i + 1)
                    dotimers(func_list)

... После запуска runtests((percent_, format_, format2_, concat_), runs=5)я обнаружил, что метод% примерно в два раза быстрее других на этих маленьких строках. Метод concat всегда был самым медленным (едва ли). Были очень крошечные различия при переключении позиций вformat() методе , но переключение позиций всегда было как минимум на 0,01 медленнее, чем метод обычного формата.

Пример результатов тестирования:

    test concat_()  : 0.62  (0.61 to 0.63)
    test format_()  : 0.56  (consistently 0.56)
    test format2_() : 0.58  (0.57 to 0.59)
    test percent_() : 0.34  (0.33 to 0.35)

Я запустил их, потому что я использую конкатенацию строк в своих скриптах, и мне было интересно, сколько это стоит. Я запускал их в разном порядке, чтобы убедиться, что ничто не мешает и не улучшает производительность в первую или в последнюю очередь. На стороне записки, я бросил в некоторых длинных строковых генераторах в эти функции , как "%s" + ("a" * 1024)и регулярный CONCAT был почти в 3 раза быстрее (1,1 против 2,8) , как с использованием formatи %методов. Думаю, это зависит от струн и от того, чего вы пытаетесь достичь. Если производительность действительно имеет значение, возможно, лучше попробовать разные вещи и протестировать их. Я предпочитаю удобочитаемость скорости, если только скорость не становится проблемой, но это только я. ТАК не понравилась моя копия / вставка, мне пришлось поставить 8 пробелов, чтобы все выглядело правильно. Я обычно использую 4.

Си Джей Велборн
источник
1
Вы должны серьезно подумать о том, что и как профилируете. Во-первых, ваш concat медленный, потому что в нем есть два типа str. Со строками результат противоположный, поскольку конкатенация строк на самом деле быстрее, чем все альтернативы, когда речь идет только о трех строках.
Юстус Вингерт
@JustusWingert, ему уже два года. Я многому научился с тех пор, как опубликовал этот «тест». Честно говоря, в наши дни я использую str.format()и str.join()более обычную конкатенацию. Я также слежу за «f-strings» из PEP 498 , который недавно был принят. Что касается str()вызовов, влияющих на производительность, я уверен, что вы правы. Я понятия не имел, насколько дорогими были вызовы функций в то время. Я по-прежнему считаю, что тесты нужно проводить, когда есть сомнения.
Cj Welborn
После быстрого теста join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), кажется join, тоже медленнее, чем в процентах.
Габор 08
4

Помните, что стилистические решения - это практические решения, если вы когда-нибудь планируете поддерживать или отлаживать свой код :-) Есть известная цитата Кнута (возможно, цитирующего Хора?): «Мы должны забыть о небольшой эффективности, скажем, примерно в 97% случаев: преждевременная оптимизация - корень всех зол ».

Пока вы будете осторожны, чтобы (сказать) не превратить задачу O (n) в задачу O (n 2 ), я бы выбрал то, что вам будет легче понять.

Джон Фухи
источник
0

Я использую замену везде, где могу. Я использую конкатенацию только в том случае, если я создаю строку, например, в цикле for.

Draemon
источник
7
«создание строки в цикле for» - часто это тот случай, когда вы можете использовать '' .join и выражение-генератор ..
Джон Фухи
-1

На самом деле, правильнее всего в этом случае (строительные пути) использовать os.path.join. Не конкатенация или интерполяция строк

Hoskeri
источник
1
это верно для путей операционной системы (например, в вашей файловой системе), но не при построении URI, как в этом примере. URI всегда имеют разделитель "/".
Андре Блюм