Łączenie ciągów a podstawianie ciągów w Pythonie

98

W Pythonie umyka mi to, gdzie i kiedy używać łączenia ciągów w porównaniu z zastępowaniem ciągów. Ponieważ konkatenacja strun spowodowała duży wzrost wydajności, czy jest to (staje się coraz bardziej) decyzją stylistyczną, a nie praktyczną?

Na konkretny przykład, jak należy obsłużyć tworzenie elastycznych identyfikatorów 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)

Edycja: Pojawiły się również sugestie dotyczące dołączania do listy ciągów i używania nazwanych podstawień. To są warianty dotyczące głównego tematu, czyli: w jaki sposób należy to zrobić w jakim czasie? Dzięki za odpowiedzi!

gotgenes
źródło
Zabawne, w Rubim interpolacja ciągów jest generalnie szybsza niż konkatenacja ...
Keltia
zapomniałeś zwrotu „” .join ([DOMAIN, QUESTIONS, str (q_num)])
Jimmy,
Nie jestem ekspertem od Rubiego, ale założę się, że interpolacja jest szybsza, ponieważ w Rubim ciągi znaków są zmienne. W Pythonie ciągi znaków są niezmiennymi sekwencjami.
gotgenes
1
mały komentarz na temat identyfikatorów URI. Identyfikatory URI nie są dokładnie takie same, jak ciągi. Istnieją identyfikatory URI, więc musisz być bardzo ostrożny podczas ich łączenia lub porównywania. Przykład: serwer dostarczający swoje reprezentacje przez http na porcie 80. example.org (bez słowa na końcu) example.org/ (ukośnik) example.org:80/ (slah + port 80) to ten sam adres Uri, ale nie to samo strunowy.
karlcow

Odpowiedzi:

55

Według mojego komputera konkatenacja jest (znacznie) szybsza. Ale stylistycznie jestem gotów zapłacić cenę zastąpienia, jeśli wydajność nie jest krytyczna. Cóż, a jeśli potrzebuję formatowania, nie ma potrzeby nawet zadawać pytania ... nie ma innego wyjścia, jak tylko użyć interpolacji / szablonów.

>>> 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
Vinko Vrsalovic
źródło
10
czy robiłeś testy z prawdziwymi dużymi łańcuchami (jak 100000 znaków)?
drnk
24

Nie zapomnij o nazwanym podstawieniu:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()
za dużo php
źródło
4
Ten kod ma co najmniej 2 złe praktyki programistyczne: oczekiwanie zmiennych globalnych (domena i pytania nie są zadeklarowane wewnątrz funkcji) i przekazywanie większej liczby zmiennych niż potrzeba do funkcji format (). Obniżanie głosów, ponieważ ta odpowiedź uczy złych praktyk kodowania.
jperelli
12

Uważaj na łączenie ciągów w pętli! Koszt konkatenacji ciągów znaków jest proporcjonalny do długości wyniku. Pętla prowadzi prosto do krainy N-kwadrat. Niektóre języki optymalizują konkatenację do ostatnio przydzielonego ciągu, ale ryzykowne jest liczenie na kompilator, który zoptymalizuje algorytm kwadratowy do liniowego. Najlepiej używać prymitywu ( join?), Który pobiera całą listę ciągów, wykonuje jedną alokację i łączy je wszystkie za jednym razem.

Norman Ramsey
źródło
16
To nie jest aktualne. W najnowszych wersjach języka Python ukryty bufor ciągów jest tworzony podczas łączenia ciągów w pętli.
Seun Osewa
5
@Seun: Tak, jak powiedziałem, niektóre języki zostaną zoptymalizowane, ale jest to ryzykowna praktyka.
Norman Ramsey
11

„Ponieważ konkatenacja strun spowodowała duży wzrost wydajności ...”

Jeśli wydajność ma znaczenie, dobrze jest wiedzieć.

Jednak problemy z wydajnością, które widziałem, nigdy nie sprowadzały się do operacji na łańcuchach. Generalnie mam problemy z operacjami I / O, sortowaniem i O ( n 2 ), które stanowią wąskie gardła.

Dopóki operacje na strunach nie ograniczą wydajności, będę się trzymać rzeczy oczywistych. Przeważnie jest to podstawienie, gdy jest to jedna linia lub mniej, konkatenacja, gdy ma to sens, i narzędzie szablonu (takie jak Mako), gdy jest duże.

S.Lott
źródło
10

To, co chcesz połączyć / interpolować i jak chcesz sformatować wynik, powinno wpłynąć na twoją decyzję.

  • Interpolacja ciągów umożliwia łatwe dodawanie formatowania. W rzeczywistości twoja wersja z interpolacją ciągów nie robi tego samego, co twoja wersja z konkatenacją; w rzeczywistości dodaje dodatkowy ukośnik przed q_numparametrem. Aby zrobić to samo, musiałbyś napisać return DOMAIN + QUESTIONS + "/" + str(q_num)w tym przykładzie.

  • Interpolacja ułatwia formatowanie liczb; "%d of %d (%2.2f%%)" % (current, total, total/current)byłby znacznie mniej czytelny w formie konkatenacji.

  • Konkatenacja jest przydatna, gdy nie masz ustalonej liczby elementów do zestrojenia.

Wiedz też, że Python 2.6 wprowadza nową wersję interpolacji ciągów, zwaną szablonami ciągów :

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

Tworzenie szablonów łańcuchów ma ostatecznie zastąpić interpolację%, ale myślę, że to nie nastąpi przez dłuższy czas.

Tim Lesher
źródło
Cóż, stanie się to za każdym razem, gdy zdecydujesz się przejść na Python 3.0. Zobacz także komentarz Petera, aby dowiedzieć się, że i tak możesz wykonywać nazwane podstawienia za pomocą operatora%.
John Fouhy,
„Konkatenacja jest przydatna, gdy nie masz ustalonej liczby elementów do zestrojenia”. - Masz na myśli listę / tablicę? W takim razie czy nie mógłbyś po prostu do nich dołączyć?
strager
„Czy nie mógłbyś po prostu do nich dołączyć?” - Tak (zakładając, że chcesz mieć jednolite separatory między elementami). Listy i wyrażenia generatora działają świetnie z string.join.
Tim Lesher,
1
„Cóż, stanie się to zawsze, gdy zdecydujesz się przejść na Python 3.0” - Nie, py3k nadal obsługuje operatora%. Kolejnym możliwym punktem wycofania jest 3.1, więc wciąż jest w nim trochę życia.
Tim Lesher,
2
2 lata później ... python 3.2 zbliża się do premiery, a interpolacja stylu% nadal działa.
Corey Goldberg
8

Właśnie testowałem szybkość różnych metod łączenia / zastępowania ciągów z ciekawości. Wyszukiwarka google na ten temat przywiodła mnie tutaj. Pomyślałem, że opublikuję wyniki moich testów w nadziei, że to pomoże komuś zdecydować.

    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)

... Po uruchomieniu runtests((percent_, format_, format2_, concat_), runs=5)zauważyłem, że metoda% była około dwa razy szybsza niż inne na tych małych strunach. Metoda concat była zawsze najwolniejsza (ledwo). Wystąpiły bardzo drobne różnice podczas zmiany pozycji w format()metodzie, ale zmiana pozycji była zawsze co najmniej 0,01 wolniejsza niż w przypadku metody standardowego formatu.

Przykładowe wyniki badań:

    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)

Uruchomiłem je, ponieważ używam konkatenacji ciągów w moich skryptach i zastanawiałem się, jaki jest koszt. Uruchomiłem je w różnych kolejności, aby upewnić się, że nic nie przeszkadza lub uzyskuje lepszą wydajność jako pierwszy lub ostatni. Na marginesie, do tych funkcji "%s" + ("a" * 1024)dodałem kilka dłuższych generatorów ciągów, a zwykłe konkatowanie było prawie 3 razy szybsze (1,1 vs 2,8) niż przy użyciu metod formati %. Myślę, że to zależy od strun i tego, co próbujesz osiągnąć. Jeśli wydajność naprawdę ma znaczenie, lepiej spróbować różnych rzeczy i je przetestować. Zwykle przedkładam czytelność zamiast szybkości, chyba że szybkość stanie się problemem, ale to tylko ja. Więc nie podobało mi się moje kopiowanie / wklejanie, musiałem umieścić 8 spacji na wszystkim, aby wyglądało dobrze. Zwykle używam 4.

Cj Welborn
źródło
1
Powinieneś poważnie rozważyć, w jaki sposób profilujesz. Po pierwsze, twoje konkatowanie jest powolne, ponieważ masz w nim dwa rzuty str. W przypadku łańcuchów wynik jest odwrotny, ponieważ konkatowanie ciągów jest w rzeczywistości szybsze niż wszystkie alternatywy, gdy chodzi tylko o trzy ciągi.
Justus Wingert
@JustusWingert, to ma teraz dwa lata. Wiele się nauczyłem, odkąd opublikowałem ten „test”. Szczerze mówiąc, te dni używam str.format()i str.join()na normalnym konkatenacji. Zwracam też uwagę na „f-strings” z PEP 498 , który został niedawno zaakceptowany. Jeśli chodzi o str()połączenia wpływające na wydajność, jestem pewien, że masz rację. Nie miałem wtedy pojęcia, jak drogie były wywołania funkcji w tamtym czasie. Nadal uważam, że testy należy wykonywać, gdy są jakiekolwiek wątpliwości.
Cj Welborn
Po szybkim teście z join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), wydaje się, że joinjest wolniejszy niż procent.
hałaśliwy
4

Pamiętaj, decyzje stylistyczne decyzjami praktycznymi, jeśli kiedykolwiek planujesz utrzymywać lub debugować swój kod :-) Znany cytat Knutha (prawdopodobnie cytując Hoare'a?): „Powinniśmy zapomnieć o małych wydajnościach, powiedzmy w 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła ”.

Tak długo, jak uważasz, aby (powiedzieć) nie zamieniać zadania O (n) w zadanie O (n 2 ), wybrałbym to, co uznasz za najłatwiejsze do zrozumienia.

John Fouhy
źródło
0

Stosuję substytucję, gdzie tylko mogę. Używam konkatenacji tylko wtedy, gdy buduję łańcuch w, powiedzmy, pętli for.

Draemon
źródło
7
„budowanie łańcucha w pętli for” - często jest to przypadek, w którym można użyć „” .join i wyrażenia generatora ..
John Fouhy,
-1

Właściwie właściwą rzeczą do zrobienia w tym przypadku (budowanie ścieżek) jest użycie os.path.join. Nie konkatenacja ciągów ani interpolacja

hoskeri
źródło
1
jest to prawdą dla ścieżek systemu operacyjnego (jak w systemie plików), ale nie podczas konstruowania identyfikatora URI, jak w tym przykładzie. Identyfikatory URI zawsze mają „/” jako separator.
Andre Blum