Jaka jest najbardziej wydajna metoda konkatenacji ciągów w Pythonie?

148

Czy istnieje wydajna metoda konkatenacji ciągów masy w Pythonie (jak StringBuilder w C # lub StringBuffer w Javie)? Znalazłem tutaj następujące metody :

  • Proste łączenie przy użyciu +
  • Korzystanie z listy ciągów i joinmetody
  • Korzystanie UserStringz MutableStringmodułu
  • Korzystanie z tablicy znaków i arraymodułu
  • Korzystanie cStringIOz StringIOmodułu

Ale czego używają lub sugerują eksperci i dlaczego?

[ Powiązane pytanie tutaj ]

mshsayem
źródło
1
Podobne pytanie: stackoverflow.com/questions/476772
Peter Mortensen
Aby połączyć znane fragmenty w jeden, Python 3.6 będzie miał f''ciągi formatujące, które będą szybsze niż jakiekolwiek alternatywy w poprzednich wersjach Pythona.
Antti Haapala,

Odpowiedzi:

127

Może Cię to zainteresować: Anegdota optymalizacyjna Guido. Chociaż warto również pamiętać, że jest to stary artykuł i poprzedza istnienie takich rzeczy jak ''.join(chociaż string.joinfieldswydaje mi się, że jest mniej więcej to samo)

W związku z tym arraymoduł może być najszybszy, jeśli uda się włożyć w niego problem. Ale ''.joinprawdopodobnie jest wystarczająco szybki i ma tę zaletę, że jest idiomatyczny, a przez to łatwiejszy do zrozumienia dla innych programistów Pythona.

Wreszcie złota zasada optymalizacji: nie optymalizuj, chyba że wiesz, że musisz, i mierz zamiast zgadywać.

Za pomocą timeitmodułu możesz mierzyć różne metody . Które mogą powiedzieć ci, który jest najszybszy, zamiast przypadkowych nieznajomych na domysły przygotowywania www.

John Fouhy
źródło
1
Chcąc dodać do kwestii, kiedy należy optymalizować: pamiętaj, aby przeprowadzić testy w najgorszych przypadkach. Na przykład mogę zwiększyć próbkę, aby mój bieżący kod przeszedł od 0,17 sekundy do 170 sekund. Cóż, chcę testować na większych próbkach, ponieważ jest tam mniej zróżnicowania.
Flipper
2
„Nie optymalizuj, dopóki nie wiesz, że musisz”. Chyba że używasz tylko nominalnie innego idiomu i możesz uniknąć przeróbki kodu przy niewielkim dodatkowym wysiłku.
jeremyjjbrown
1
Jedynym miejscem, o którym wiesz, że potrzebujesz, jest rozmowa kwalifikacyjna (która jest zawsze świetnym momentem na pogłębienie zrozumienia). Niestety nie znalazłem ŻADNEGO współczesnego artykułu na ten temat. (1) Czy ciąg Java / C # nadal jest taki zły w 2017 roku? (2) A co z C ++? (3) Teraz opowiedz o najnowszych i najlepszych w Pythonie, koncentrując się na przypadkach, w których musimy wykonać miliony konkatenacji. Czy możemy ufać, że sprzężenie będzie działać w czasie liniowym?
user1854182
Co oznacza „wystarczająco szybko” .join()? Głównym pytaniem jest, czy a) utworzyć kopię ciągu do konkatenacji (podobnie jak s = s + 'abc'), co wymaga O (n) runtime, czy b) po prostu dołączyć do istniejącego ciągu bez tworzenia kopii, co wymaga O (1) ?
CGFoX
64

''.join(sequenceofstrings) jest to, co zwykle działa najlepiej - najprostsze i najszybsze.

Alex Martelli
źródło
3
@mshsayem, w Pythonie sekwencją może być dowolny wyliczalny obiekt, nawet funkcja.
Nick Dandoulakis
2
Uwielbiam ten ''.join(sequence)idiom. Jest to szczególnie przydatne do tworzenia list oddzielonych przecinkami: ', '.join([1, 2, 3])podaje ciąg '1, 2, 3'.
Andrew Keeton
7
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- w tym przypadku argumentem do złączenia jest iterator utworzony za pomocą wyrażenia generatora. Nie ma tymczasowej listy, która została utworzona.
balpha
2
@balpha: a mimo to wersja generatora jest wolniejsza niż wersja ze zrozumieniem listy: C: \ temp> python -mtimeit "'' .join (chr (x) for x in xrange (65,91))" 100000 pętli, najlepsze z 3: 9,71 usek na pętlę C: \ temp> python -mtimeit "'' .join ([chr (x) for x in xrange (65,91)])" 100000 pętli, najlepsza z 3: 7,1
usek
1
@hughdbrown, tak, kiedy masz wolną pamięć poza wazoo (typowy przypadek timeit), listcomp może być lepiej zoptymalizowany niż genexp, często o 20-30%. Kiedy napięta pamięć jest inna - trudna do odtworzenia w czasie, chociaż! -)
Alex Martelli
58

Python 3.6 zmienił grę pod kątem konkatenacji ciągów znanych komponentów za pomocą dosłownej interpolacji ciągów .

Biorąc pod uwagę przypadek testowy z odpowiedzi mkoistinena , mając ciągi

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'

Pretendenci są

  • f'http://{domain}/{lang}/{path}'- 0,151 µs

  • 'http://%s/%s/%s' % (domain, lang, path) - 0,321 µs

  • 'http://' + domain + '/' + lang + '/' + path - 0,356 µs

  • ''.join(('http://', domain, '/', lang, '/', path))- 0,249 µs (zauważ, że budowanie krotki o stałej długości jest nieco szybsze niż tworzenie listy o stałej długości).

Zatem obecnie najkrótszy i najpiękniejszy możliwy kod jest również najszybszy.

W wersjach alfa Pythona 3.6 implementacja f''łańcuchów była najwolniejsza z możliwych - w rzeczywistości wygenerowany kod bajtowy jest prawie równoważny z ''.join()przypadkiem niepotrzebnych wywołań, do str.__format__których bez argumentów po prostu zwracałby selfniezmieniony. Te nieefektywności zostały usunięte przed wersją 3.6.

Szybkość można porównać z najszybszą metodą dla Pythona 2, czyli +konkatenacją na moim komputerze; a to zajmuje 0,203 µs przy 8-bitowych łańcuchach i 0,259 µs, jeśli wszystkie łańcuchy są Unicode.

Antti Haapala
źródło
38

To zależy od tego, co robisz.

Po Pythonie 2.5, konkatenacja ciągów znaków z operatorem + jest dość szybka. Jeśli łączysz tylko kilka wartości, najlepiej działa operator +:

>>> x = timeit.Timer(stmt="'a' + 'b'")
>>> x.timeit()
0.039999961853027344

>>> x = timeit.Timer(stmt="''.join(['a', 'b'])")
>>> x.timeit()
0.76200008392333984

Jeśli jednak składasz łańcuch w pętli, lepiej jest użyć metody łączenia list:

>>> join_stmt = """
... joined_str = ''
... for i in xrange(100000):
...   joined_str += str(i)
... """
>>> x = timeit.Timer(join_stmt)
>>> x.timeit(100)
13.278000116348267

>>> list_stmt = """
... str_list = []
... for i in xrange(100000):
...   str_list.append(str(i))
... ''.join(str_list)
... """
>>> x = timeit.Timer(list_stmt)
>>> x.timeit(100)
12.401000022888184

... ale zauważ, że musisz złożyć stosunkowo dużą liczbę strun, zanim różnica stanie się zauważalna.

Jason Baker
źródło
2
1) W pierwszym pomiarze prawdopodobnie budowa listy zajmuje dużo czasu. Spróbuj z krotką. 2) CPython działa jednolicie dobrze, jednak inne implementacje Pythona działają znacznie gorzej z + i + =
u0b34a0f6ae
22

Zgodnie z odpowiedzią Johna Fouhy'ego, nie optymalizuj, chyba że musisz, ale jeśli jesteś tutaj i zadajesz to pytanie, może to być właśnie dlatego, że musisz . W moim przypadku potrzebowałem złożyć kilka adresów URL ze zmiennych łańcuchowych ... szybko. Zauważyłem, że nikt (jak dotąd) nie rozważał metody formatu ciągów, więc pomyślałem, że spróbuję tego i, głównie ze względu na niewielkie zainteresowanie, pomyślałem, że wrzucę tam operator interpolacji ciągów dla dobrego pomiaru. Szczerze mówiąc, nie sądziłem, że którykolwiek z nich będzie się wiązał z bezpośrednią operacją „+” lub „.join ()”. Ale zgadnij co? W moim systemie Python 2.7.5 operator interpolacji ciągów rządzi wszystkimi, a string.format () jest najgorszy:

# concatenate_test.py

from __future__ import print_function
import timeit

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'
iterations = 1000000

def meth_plus():
    '''Using + operator'''
    return 'http://' + domain + '/' + lang + '/' + path

def meth_join():
    '''Using ''.join()'''
    return ''.join(['http://', domain, '/', lang, '/', path])

def meth_form():
    '''Using string.format'''
    return 'http://{0}/{1}/{2}'.format(domain, lang, path)

def meth_intp():
    '''Using string interpolation'''
    return 'http://%s/%s/%s' % (domain, lang, path)

plus = timeit.Timer(stmt="meth_plus()", setup="from __main__ import meth_plus")
join = timeit.Timer(stmt="meth_join()", setup="from __main__ import meth_join")
form = timeit.Timer(stmt="meth_form()", setup="from __main__ import meth_form")
intp = timeit.Timer(stmt="meth_intp()", setup="from __main__ import meth_intp")

plus.val = plus.timeit(iterations)
join.val = join.timeit(iterations)
form.val = form.timeit(iterations)
intp.val = intp.timeit(iterations)

min_val = min([plus.val, join.val, form.val, intp.val])

print('plus %0.12f (%0.2f%% as fast)' % (plus.val, (100 * min_val / plus.val), ))
print('join %0.12f (%0.2f%% as fast)' % (join.val, (100 * min_val / join.val), ))
print('form %0.12f (%0.2f%% as fast)' % (form.val, (100 * min_val / form.val), ))
print('intp %0.12f (%0.2f%% as fast)' % (intp.val, (100 * min_val / intp.val), ))

Wyniki:

# python2.7 concatenate_test.py
plus 0.360787868500 (90.81% as fast)
join 0.452811956406 (72.36% as fast)
form 0.502608060837 (65.19% as fast)
intp 0.327636957169 (100.00% as fast)

Jeśli użyję krótszej domeny i krótszej ścieżki, nadal wygrywa interpolacja. Różnica jest jednak wyraźniejsza w przypadku dłuższych sznurków.

Teraz, gdy miałem ładny skrypt testowy, testowałem również pod Pythonem 2.6, 3.3 i 3.4, oto wyniki. W Pythonie 2.6 operator plus jest najszybszy! W Pythonie 3 dołączanie wygrywa. Uwaga: te testy są bardzo powtarzalne w moim systemie. Tak więc „plus” jest zawsze szybszy w 2.6, „intp” jest zawsze szybszy w 2.7, a „join” jest zawsze szybszy w Pythonie 3.x.

# python2.6 concatenate_test.py
plus 0.338213920593 (100.00% as fast)
join 0.427221059799 (79.17% as fast)
form 0.515371084213 (65.63% as fast)
intp 0.378169059753 (89.43% as fast)

# python3.3 concatenate_test.py
plus 0.409130576998 (89.20% as fast)
join 0.364938726001 (100.00% as fast)
form 0.621366866995 (58.73% as fast)
intp 0.419064424001 (87.08% as fast)

# python3.4 concatenate_test.py
plus 0.481188605998 (85.14% as fast)
join 0.409673971997 (100.00% as fast)
form 0.652010936996 (62.83% as fast)
intp 0.460400978001 (88.98% as fast)

# python3.5 concatenate_test.py
plus 0.417167026084 (93.47% as fast)
join 0.389929617057 (100.00% as fast)
form 0.595661019906 (65.46% as fast)
intp 0.404455224983 (96.41% as fast)

Wyciągnięta lekcja:

  • Czasami moje założenia są całkowicie błędne.
  • Przetestuj pod kątem środowiska systemowego. będziesz pracować w produkcji.
  • Interpolacja ciągów jeszcze nie umarła!

tl; dr:

  • Jeśli używasz wersji 2.6, użyj operatora +.
  • jeśli używasz wersji 2.7, użyj operatora „%”.
  • jeśli używasz 3.x użyj '' .join ().
mkoistinen
źródło
2
Uwaga: dosłowna interpolacja ciągów jest jeszcze szybsza dla wersji 3.6+:f'http://{domain}/{lang}/{path}'
TemporalWolf
1
Ponadto, .format()ma trzy formy, w kolejności od szybkiego do wolnego: "{}".format(x), "{0}".format(x),"{x}".format(x=x)
TemporalWolf
Prawdziwa lekcja: kiedy twoja problematyczna domena jest mała, np. Komponowanie krótkich stringów, metoda najczęściej nie ma znaczenia. I nawet jeśli ma to znaczenie, np. Naprawdę budujesz milion strun, narzut często ma większe znaczenie. Jest to typowy objaw zamartwiania się niewłaściwym problemem. Dopiero gdy narzut nie jest znaczący, np. Przy budowaniu całej książki jako ciągu, różnica metod zaczyna mieć znaczenie.
Hui Zhou
7

w dużej mierze zależy to od względnych rozmiarów nowego łańcucha po każdym nowym połączeniu. Za pomocą +operatora dla każdego konkatenacji tworzony jest nowy łańcuch. Jeśli ciągi pośredniczące są stosunkowo długie, +stają się coraz wolniejsze, ponieważ nowy ciąg pośredni jest przechowywany.

Rozważ ten przypadek:

from time import time
stri=''
a='aagsdfghfhdyjddtyjdhmfghmfgsdgsdfgsdfsdfsdfsdfsdfsdfddsksarigqeirnvgsdfsdgfsdfgfg'
l=[]
#case 1
t=time()
for i in range(1000):
    stri=stri+a+repr(i)
print time()-t

#case 2
t=time()
for i in xrange(1000):
    l.append(a+repr(i))
z=''.join(l)
print time()-t

#case 3
t=time()
for i in range(1000):
    stri=stri+repr(i)
print time()-t

#case 4
t=time()
for i in xrange(1000):
    l.append(repr(i))
z=''.join(l)
print time()-t

Wyniki

1 0,00493192672729

2 0.000509023666382

3 0.00042200088501

4 0.000482797622681

W przypadku 1 & 2 dodajemy duży ciąg i funkcja join () działa około 10 razy szybciej. W przypadku 3 i 4 dodajemy mały ciąg i znak „+” działa nieco szybciej

David Bielen
źródło
3

Znalazłem się w sytuacji, w której potrzebowałem dołączyć ciąg o nieznanym rozmiarze. Oto wyniki testów porównawczych (python 2.7.3):

$ python -m timeit -s 's=""' 's+="a"'
10000000 loops, best of 3: 0.176 usec per loop
$ python -m timeit -s 's=[]' 's.append("a")'
10000000 loops, best of 3: 0.196 usec per loop
$ python -m timeit -s 's=""' 's="".join((s,"a"))'
100000 loops, best of 3: 16.9 usec per loop
$ python -m timeit -s 's=""' 's="%s%s"%(s,"a")'
100000 loops, best of 3: 19.4 usec per loop

To wydaje się wskazywać, że najszybszy jest znak „+ =”. Wyniki z linku Skymind są nieco nieaktualne.

(Zdaję sobie sprawę, że drugi przykład nie jest kompletny, ostateczna lista musiałaby zostać połączona. To jednak pokazuje, że samo przygotowanie listy trwa dłużej niż konkatowanie ciągów).

MattK
źródło
Otrzymuję mniej niż 1 sekundę dla trzeciego i czwartego testu. Dlaczego osiągasz takie dobre czasy? pastebin.com/qabNMCHS
bad_keypoints
@ronnieaka: We wszystkich testach osiąga mniej niż 1 sekundę. Dostaje> 1 µs dla 3 i 4, czego nie zrobiłeś. W tych testach mam również wolniejsze czasy (na Pythonie 2.7.5, Linux). Może to być procesor, wersja, flagi kompilacji, kto wie.
Thanatos
Te wyniki testów porównawczych są bezużyteczne. Zwłaszcza w pierwszym przypadku, który nie wykonuje żadnej konkatenacji ciągów, po prostu zwraca drugą wartość ciągu w stanie nienaruszonym.
Antti Haapala
3

Rok później przetestujmy odpowiedź mkoistinena w Pythonie 3.4.3:

  • plus 0,963564149000 (95,83% tak szybko)
  • dołącz 0,923408469000 (100,00% tak szybko)
  • formularz 1.501130934000 (61,51% tak szybko)
  • intp 1.019677452000 (90,56% tak szybko)

Nic się nie zmieniło. Łączenie jest nadal najszybszą metodą. Ponieważ intp jest prawdopodobnie najlepszym wyborem pod względem czytelności, mimo wszystko możesz chcieć użyć intp.

ramsch
źródło
1
Może to być dodatek do odpowiedzi mkoistinen, ponieważ brakuje jej pełnej odpowiedzi (lub przynajmniej dodaj kod, którego używasz).
Trilarion
1

Zainspirowany testami porównawczymi @ JasonBaker, oto prosty test porównujący 10 "abcdefghijklmnopqrstuvxyz"strun, pokazujący, że .join()jest szybszy; nawet przy tak niewielkim wzroście zmiennych:

Wiązanie

>>> x = timeit.Timer(stmt='"abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz"')
>>> x.timeit()
0.9828147209324385

Przystąp

>>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
>>> x.timeit()
0.6114138159765048
W
źródło
Spójrz na zaakceptowaną odpowiedź (przewiń w dół) na to pytanie: stackoverflow.com/questions/1349311/…
mshsayem
1

Dla małego zestawu z krótkich ciągów (czyli 2 lub 3 struny nie więcej niż kilka znaków), oraz jest jeszcze szybsza. Korzystanie ze wspaniałego skryptu mkoistinen w Pythonie 2 i 3:

plus 2.679107467004 (100.00% as fast)
join 3.653773699996 (73.32% as fast)
form 6.594011374000 (40.63% as fast)
intp 4.568015249999 (58.65% as fast)

Więc kiedy twój kod wykonuje ogromną liczbę oddzielnych małych konkatenacji, plus jest preferowanym sposobem, jeśli szybkość ma kluczowe znaczenie.

user7505681
źródło
1

Prawdopodobnie „nowe f-stringi w Pythonie 3.6” to najbardziej efektywny sposób łączenia łańcuchów.

Używając% s

>>> timeit.timeit("""name = "Some"
... age = 100
... '%s is %s.' % (name, age)""", number = 10000)
0.0029734770068898797

Korzystanie z .format

>>> timeit.timeit("""name = "Some"
... age = 100
... '{} is {}.'.format(name, age)""", number = 10000)
0.004015227983472869

Korzystanie f

>>> timeit.timeit("""name = "Some"
... age = 100
... f'{name} is {age}.'""", number = 10000)
0.0019175919878762215

Źródło: https://realpython.com/python-f-strings/

SuperNova
źródło