З'єднання рядків проти підстановки рядків у 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)

Редагувати: Також були пропозиції щодо приєднання до списку рядків та використання названої підстановки. Це варіанти на центральну тему, що полягає в тому, який спосіб - це правильний спосіб зробити це в який час? Дякуємо за відповіді!


Смішно, в Рубі струнна інтерполяція, як правило, швидша, ніж конкатенація ...
Келтія

ви забули повернення "" .join ([ДОМАН, ЗАПИТАННЯ, str (q_num)])
Джиммі

Я не фахівець Рубі, але я б став би, що інтерполяція швидша, тому що в Ruby рядки змінюються. Струни - це незмінні послідовності в Python.
gotgenes

1
лише невеликий коментар про URI. URI не зовсім схожі на рядки. Є URI, тому ви повинні бути дуже обережними, коли об'єднуєте або порівнюєте їх. Приклад: сервер, що подає свої представлення через http на порт 80. example.org (без вбивства в кінці) example.org/ (слэш) example.org:80/ (slah + порт 80) - це той самий урі, але не той самий рядок.
Карлков

Відповіді:


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

4
Цей код має щонайменше 2 погані практики програмування: очікування глобальних змінних (домен і питання не оголошуються всередині функції) і передача більшої кількості змінних, ніж потрібно, для функції формату (). Нахил, тому що ця відповідь вчить поганим методам кодування.
jperelli

12

Будьте обережні, що поєднуються рядки в циклі! Вартість конкатенації рядків пропорційна довжині результату. Цикл веде вас прямо до землі N-квадрата. Деякі мови оптимізують конкатенацію до останнього виділеного рядка, але ризикувати розраховувати на компілятор, щоб оптимізувати ваш квадратичний алгоритм до лінійного. Найкраще використовувати примітив ( join?), Який займає цілий список рядків, робить одне виділення і об'єднує їх усі за один раз.


16
Це не актуально. В останніх версіях python під час об'єднання рядків у циклі створюється прихований буфер рядків.
Seun Osewa

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)було б набагато менш читабельним у формі конкатенації.

  • Зв'язування корисно, коли у вас немає фіксованої кількості елементів для string-ize.

Крім того, знайте, що Python 2.6 представляє нову версію інтерполяції рядків, що називається templating string :

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

Шаблони рядків плануються з часом, щоб замінити% -інтерполяцію, але це не відбудеться досить довго, я думаю.


Що ж, це станеться кожного разу, коли ви вирішите перейти на python 3.0. Також дивіться коментар Петра щодо того, що ви можете робити заміни з ім'ям% оператора%.
Джон Фугі

"Зв'язування корисно, коли у вас немає фіксованої кількості елементів для string-ize." - Ви маєте на увазі список / масив? У такому випадку ви не могли просто приєднатись до них?
страгер

"Не могли ви просто приєднатись до них ()?" - Так (якщо припустити, що ви хочете рівномірно розділити між елементами). Зрозуміння списків та генераторів чудово працює з string.join.
Тім Лешер

1
"Ну, це станеться, коли ви вирішите перейти на python 3.0" - Ні, py3k все ще підтримує оператора%. Наступна можлива точка депресії - 3,1, тому в неї ще є деяке життя.
Тім Лешер

2
Через 2 роки ... python 3.2 наближається до випуску, і інтерполяція% style все ще чудова.
Корі Голдберг

8

Я просто перевіряв швидкість різних методів конкатенації / заміщення рядків з цікавості. Гугл-пошук на цю тему привів мене сюди. Я думав, що опублікую свої результати тесту в надії, що це може допомогти комусь прийняти рішення.

    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() методі, але перемикання позицій завжди було щонайменше .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)звичайний конмат був майже в 3 рази швидшим (1,1 проти 2,8), ніж за допомогою методів formatта %. Я думаю, це залежить від рядків і того, що ви намагаєтесь досягти. Якщо продуктивність дійсно має значення, можливо, краще спробувати різні речі та перевірити їх. Я схильний вибирати читабельність над швидкістю, якщо швидкість не стане проблемою, але це тільки я. Так мені не сподобалася моя копія / вставка, мені довелося поставити 8 пробілів на все, щоб воно виглядало правильно. Я зазвичай використовую 4.


1
Вам слід серйозно подумати про те, як ви профілюєте. Для одного ваш конфамат повільний, тому що у вас є дві стрічки. У рядках результат протилежний, оскільки концета рядків насправді швидша за всі альтернативи, якщо мова йде лише про три рядки.
Justus Wingert

@JustusWingert, ось уже два роки. Я багато чого навчився, оскільки опублікував цей "тест". Чесно кажучи, ці дні я використовую str.format()і str.join()над нормальним конкатенацією. Я також стежу за "f-string" від PEP 498 , який нещодавно був прийнятий. Що стосується str()дзвінків, що впливають на продуктивність, я впевнений, що ви маєте рацію з цього приводу. Я поняття не мав, наскільки дорогими були функціонування в той час. Я все ще думаю, що тести потрібно робити, коли є сумніви.
Cj Welborn

Після швидкого тестування join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), схоже, joinце також повільніше відсотків.
габоровий

4

Пам'ятайте, що стилістичні рішення - це практичні рішення, якщо ви коли-небудь плануєте підтримувати чи налагоджувати свій код :-) Є відома цитата Кнута (можливо, цитування Хоара?): "Ми повинні забути про невелику ефективність, скажімо, про 97% часу: передчасна оптимізація - корінь усього зла ».

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


0

Я використовую заміну, де можу. Я використовую конкатенацію лише тоді, коли будую рядок в, наприклад, циклі for.


7
"побудова рядка в циклі for-циклу" - часто це випадок, коли ви можете використовувати "" .join та вираз генератора ..
John Fouhy

-1

Насправді правильне в цьому випадку (побудова доріжок) - це використання os.path.join. Не рядкове конкатенація чи інтерполяція


1
це справедливо для OS-шляхів (як у вашій файловій системі), але не при створенні URI, як у цьому прикладі. URI завжди має "/" як роздільник.
Андре Блюм
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.