Jaki jest preferowany sposób konkatenacji łańcucha w Pythonie?

358

Ponieważ stringnie można zmienić Pythona , zastanawiałem się, jak połączyć łańcuch bardziej efektywnie?

Mogę tak napisać:

s += stringfromelsewhere

lub tak:

s = []
s.append(somestring)

later

s = ''.join(s)

Pisząc to pytanie, znalazłem dobry artykuł mówiący na ten temat.

http://www.skymind.com/~ocrow/python_string/

Ale jest w Pythonie 2.x., więc pytanie brzmiałoby: czy coś się zmieniło w Pythonie 3?

Max
źródło

Odpowiedzi:

433

Najlepszym sposobem dołączania ciąg do zmiennej łańcuchowej jest użycie +lub +=. Jest tak, ponieważ jest czytelny i szybki. Są również tak szybkie, który wybierzesz jest kwestią gustu, ten drugi jest najczęstszy. Oto czasy z timeitmodułem:

a = a + b:
0.11338996887207031
a += b:
0.11040496826171875

Jednak ci, którzy zalecają posiadanie list i dołączanie do nich, a następnie dołączanie do nich, robią to, ponieważ przypuszczenie dodania łańcucha do listy jest prawdopodobnie bardzo szybkie w porównaniu z przedłużeniem łańcucha. W niektórych przypadkach może to być prawda. Tutaj, na przykład, milion dołącza jednoznakowy ciąg, najpierw do ciągu, a następnie do listy:

a += b:
0.10780501365661621
a.append(b):
0.1123361587524414

OK, okazuje się, że nawet jeśli wynikowy ciąg ma milion znaków, dołączanie było jeszcze szybsze.

Teraz spróbujmy dodać łańcuch o długości tysiąca znaków sto tysięcy razy:

a += b:
0.41823482513427734
a.append(b):
0.010656118392944336

Końcowy ciąg ma zatem długość około 100 MB. To było dość powolne, dołączanie do listy było znacznie szybsze. Że ten czas nie obejmuje finału a.join(). Jak długo to zajmie?

a.join(a):
0.43739795684814453

Ups. Okazuje się, że nawet w tym przypadku dołączanie / łączenie przebiega wolniej.

Skąd więc ta rekomendacja? Python 2?

a += b:
0.165287017822
a.append(b):
0.0132720470428
a.join(a):
0.114929914474

Cóż, dołączanie / łączenie jest tam marginalnie szybsze, jeśli używasz bardzo długich ciągów (którymi zwykle nie jesteś, jaki miałbyś ciąg, który ma 100 MB pamięci?)

Ale prawdziwym klinicerem jest Python 2.3. Gdzie nawet nie pokażę ci czasu, ponieważ jest tak wolny, że jeszcze się nie skończył. Te testy nagle trwają minuty . Z wyjątkiem append / join, który jest tak samo szybki jak w późniejszych Pythonach.

Tak. Łączenie strun było bardzo powolne w Pythonie w epoce kamienia łupanego. Ale w wersji 2.4 już go nie ma (a przynajmniej Python 2.4.7), więc zalecenie użycia append / join stało się nieaktualne w 2008 roku, kiedy Python 2.3 przestał być aktualizowany i powinieneś przestać go używać. :-)

(Aktualizacja: Okazuje się, gdy przeprowadziłem testowanie ostrożniej niż przy użyciu +i +=jest szybsze dla dwóch łańcuchów w Pythonie 2.3. Zalecenia dotyczące używania ''.join()muszą być nieporozumieniem)

Jest to jednak CPython. Inne wdrożenia mogą mieć inne obawy. I to tylko kolejny powód, dla którego przedwczesna optymalizacja jest źródłem wszelkiego zła. Nie używaj techniki, która powinna być „szybsza”, chyba że najpierw ją zmierzysz.

Dlatego „najlepszą” wersją do łączenia łańcuchów jest użycie + lub + = . A jeśli okaże się to dla ciebie powolne, co jest dość mało prawdopodobne, zrób coś innego.

Dlaczego więc używam dużo append / join w moim kodzie? Ponieważ czasami jest to wyraźniejsze. Zwłaszcza gdy wszystko, co powinieneś połączyć, powinno być oddzielone spacjami, przecinkami lub znakami nowej linii.

Lennart Regebro
źródło
10
Jeśli masz wiele ciągów znaków (n> 10) .join (lista_ciągów) jest jeszcze szybszy
Mikko Ohtamaa
11
powodem, dla którego + = jest szybki, jest hack wydajnościowy w cpython, jeśli przelicznik wynosi 1 - rozpada się na prawie wszystkich innych implementacjach Pythona (z wyjątkiem raczej specjalnie skonfigurowanej wersji pypy)
Ronny
17
Dlaczego tak bardzo się to cieszy? W jaki sposób lepiej jest zastosować algorytm, który jest skuteczny tylko na jednej konkretnej implementacji i który ma w istocie kruchy hack, aby naprawić kwadratowy algorytm czasu? Również całkowicie nie rozumiesz sensu „przedwczesna optymalizacja jest źródłem wszelkiego zła”. Ten cytat mówi o MAŁYCH optymalizacjach. Przechodzi od O (n ^ 2) do O (n), co NIE jest małą optymalizacją.
Wes
12
Oto faktyczny cytat: „Powinniśmy zapomnieć o małej wydajności, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła. Jednak nie powinniśmy tracić naszych możliwości w tak krytycznych 3%. Dobry programista nie będzie takie uspokojenie popadnie w samozadowolenie, mądrze będzie uważnie przyjrzeć się kodowi krytycznemu, ale dopiero po zidentyfikowaniu tego kodu ”
Wes
2
Nikt nie mówi, że a + b jest wolne. Jest kwadratowe, gdy wykonujesz a = a + b więcej niż raz. a + b + c nie jest wolny, powtarzam, nie powolny, ponieważ musi on przejść tylko jeden ciąg, podczas gdy musi wielokrotnie przechodzić przez poprzednie ciągi z podejściem a = a + b (zakładając, że jest w pętli jakiegoś rodzaju). Pamiętaj, że ciągi znaków są niezmienne.
Wes
52

Jeśli łączysz wiele wartości, to żadna z nich. Dołączanie listy jest drogie. Możesz do tego użyć StringIO. Zwłaszcza jeśli budujesz go przez wiele operacji.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

buf.write('foo')
buf.write('foo')
buf.write('foo')

buf.getvalue()
# 'foofoofoo'

Jeśli masz już pełną listę z innej operacji, po prostu użyj ''.join(aList)

Z python FAQ: Jaki jest najbardziej efektywny sposób łączenia wielu ciągów razem?

Obiekty str i bajty są niezmienne, dlatego konkatenacja wielu łańcuchów razem jest nieefektywna, ponieważ każda konkatenacja tworzy nowy obiekt. W ogólnym przypadku całkowity koszt czasu wykonania jest kwadratowy w całkowitej długości ciągu.

Aby zgromadzić wiele obiektów str, zalecanym idiomem jest umieszczenie ich na liście i wywołanie str.join () na końcu:

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(innym dość wydajnym idiomem jest użycie io.StringIO)

Aby zgromadzić wiele obiektów bajtów, zalecanym idiomem jest rozszerzenie obiektu bajtownicy przy użyciu konkatenacji na miejscu (operator + =):

result = bytearray()
for b in my_bytes_objects:
    result += b

Edycja: Byłem głupiutki i wyniki wkleiłem wstecz, dzięki czemu wyglądało na to, że dołączanie do listy było szybsze niż cStringIO. Dodałem również testy dla bytearray / str concat, a także drugą rundę testów przy użyciu większej listy z większymi ciągami. (python 2.7.3)

Przykład testu ipython dla dużych list ciągów

try:
    from cStringIO import StringIO
except:
    from io import StringIO

source = ['foo']*1000

%%timeit buf = StringIO()
for i in source:
    buf.write(i)
final = buf.getvalue()
# 1000 loops, best of 3: 1.27 ms per loop

%%timeit out = []
for i in source:
    out.append(i)
final = ''.join(out)
# 1000 loops, best of 3: 9.89 ms per loop

%%timeit out = bytearray()
for i in source:
    out += i
# 10000 loops, best of 3: 98.5 µs per loop

%%timeit out = ""
for i in source:
    out += i
# 10000 loops, best of 3: 161 µs per loop

## Repeat the tests with a larger list, containing
## strings that are bigger than the small string caching 
## done by the Python
source = ['foo']*1000

# cStringIO
# 10 loops, best of 3: 19.2 ms per loop

# list append and join
# 100 loops, best of 3: 144 ms per loop

# bytearray() +=
# 100 loops, best of 3: 3.8 ms per loop

# str() +=
# 100 loops, best of 3: 5.11 ms per loop
jdi
źródło
2
cStringIOnie istnieje w Py3. Użyj io.StringIOzamiast tego.
lvc
2
Jeśli chodzi o to, dlaczego wielokrotne dołączanie do łańcucha może być drogie: joelonsoftware.com/articles/fog0000000319.html
Wes
36

W Pythonie> = 3.6 nowy ciąg f jest skutecznym sposobem na konkatenację łańcucha.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'
SuperNova
źródło
8

Zalecaną metodą jest nadal dołączanie i dołączanie.

MRAB
źródło
1
Jak widać z mojej odpowiedzi, zależy to od tego, ile łańcuchów łączysz. Poświęciłem na to trochę czasu (patrz przemówienie, do którego linkowałem w komentarzach do mojej odpowiedzi) i ogólnie, chyba że jest więcej niż dziesięć, użyj +.
Lennart Regebro
1
PEP8 wspomina o tym ( python.org/dev/peps/pep-0008/#programming-recommendations ). Racjonalne jest to, że chociaż CPython ma specjalne optymalizacje do łączenia łańcuchów z + =, inne implementacje mogą tego nie robić.
Quantum7
8

Jeśli łączone łańcuchy są literałami, użyj Łańcuchowego łączenia literałów

re.compile(
        "[A-Za-z_]"       # letter or underscore
        "[A-Za-z0-9_]*"   # letter, digit or underscore
    )

Jest to przydatne, jeśli chcesz skomentować część ciągu (jak wyżej) lub jeśli chcesz użyć nieprzetworzonych ciągów lub potrójnych cudzysłowów dla części literału, ale nie dla wszystkich.

Ponieważ dzieje się tak na warstwie składni, używa zerowych operatorów konkatenacji.

droid
źródło
7

Piszesz tę funkcję

def str_join(*args):
    return ''.join(map(str, args))

Następnie możesz dzwonić, gdziekolwiek chcesz

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3
Shameem
źródło
1
str_join = lambda *str_list: ''.join(s for s in str_list)
Rick wspiera Monikę
7

Używanie w miejscu konkatenacji ciągu przez „+” jest NAJGORSZĄ metodą konkatenacji pod względem stabilności i implementacji krzyżowej, ponieważ nie obsługuje wszystkich wartości. Standard PEP8 odradza to i zachęca do używania format (), join () i append () do długotrwałego użytkowania.

Zgodnie z cytowaną sekcją „Zalecenia dotyczące programowania”:

Na przykład, nie polegaj na efektywnej implementacji CPython w miejscu łączenia konkatenacji ciągów dla instrukcji w postaci a + = b lub a = a + b. Ta optymalizacja jest krucha nawet w CPython (działa tylko dla niektórych typów) i nie jest wcale obecna w implementacjach, które nie używają przeliczania. W wrażliwych na wydajność częściach biblioteki należy zamiast tego użyć formularza .join (). Zapewni to, że konkatenacja nastąpi w czasie liniowym w różnych implementacjach.

problemy
źródło
5
Link referencyjny byłby miły :)
6

Choć nieco przestarzały, Kod Like a Pythonista: Idiomatic Python zaleca join()się + w tej sekcji . Podobnie jak PythonSpeedPerformanceTips w swojej sekcji dotyczącej łączenia łańcuchów , z następującym zastrzeżeniem:

Dokładność tej sekcji jest kwestionowana w odniesieniu do późniejszych wersji Pythona. W CPython 2.5 konkatenacja ciągów znaków jest dość szybka, chociaż może to nie dotyczyć również innych implementacji Pythona. Zobacz ConcatenationTestCode do dyskusji.

Levon
źródło
6

Jak wspomina @jdi, dokumentacja Pythona sugeruje użycie str.joinlub io.StringIOdo łączenia łańcuchów. I mówi, że programista powinien oczekiwać kwadratowego czasu +=w pętli, mimo że istnieje optymalizacja od Python 2.4. Jak mówi ta odpowiedź:

Jeśli Python wykryje, że lewy argument nie ma innych odwołań, wywołuje reallocpróbę uniknięcia kopii poprzez zmianę rozmiaru łańcucha w miejscu. To nie jest coś, na czym powinieneś kiedykolwiek polegać, ponieważ jest to szczegół implementacji i ponieważ jeśli reallocczęsto trzeba często przesuwać ciąg, wydajność i tak spada do O (n ^ 2).

Pokażę przykład kodu z prawdziwego świata, który naiwnie polegał na +=tej optymalizacji, ale nie miał zastosowania. Poniższy kod konwertuje iterowalne krótkie ciągi znaków na większe fragmenty, które mają być używane w zbiorczym interfejsie API.

def test_concat_chunk(seq, split_by):
    result = ['']
    for item in seq:
        if len(result[-1]) + len(item) > split_by: 
            result.append('')
        result[-1] += item
    return result

Ten kod może działać literacko przez wiele godzin z powodu kwadratowej złożoności czasu. Poniżej znajdują się alternatywy z sugerowanymi strukturami danych:

import io

def test_stringio_chunk(seq, split_by):
    def chunk():
        buf = io.StringIO()
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                size += buf.write(item)
            else:
                yield buf.getvalue()
                buf = io.StringIO()
                size = buf.write(item)
        if size:
            yield buf.getvalue()

    return list(chunk())

def test_join_chunk(seq, split_by):
    def chunk():
        buf = []
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                buf.append(item)
                size += len(item)
            else:
                yield ''.join(buf)                
                buf.clear()
                buf.append(item)
                size = len(item)
        if size:
            yield ''.join(buf)

    return list(chunk())

I mikro-benchmark:

import timeit
import random
import string
import matplotlib.pyplot as plt

line = ''.join(random.choices(
    string.ascii_uppercase + string.digits, k=512)) + '\n'
x = []
y_concat = []
y_stringio = []
y_join = []
n = 5
for i in range(1, 11):
    x.append(i)
    seq = [line] * (20 * 2 ** 20 // len(line))
    chunk_size = i * 2 ** 20
    y_concat.append(
        timeit.timeit(lambda: test_concat_chunk(seq, chunk_size), number=n) / n)
    y_stringio.append(
        timeit.timeit(lambda: test_stringio_chunk(seq, chunk_size), number=n) / n)
    y_join.append(
        timeit.timeit(lambda: test_join_chunk(seq, chunk_size), number=n) / n)
plt.plot(x, y_concat)
plt.plot(x, y_stringio)
plt.plot(x, y_join)
plt.legend(['concat', 'stringio', 'join'], loc='upper left')
plt.show()

mikro-benchmark

saaj
źródło
5

Możesz to zrobić na różne sposoby.

str1 = "Hello"
str2 = "World"
str_list = ['Hello', 'World']
str_dict = {'str1': 'Hello', 'str2': 'World'}

# Concatenating With the + Operator
print(str1 + ' ' + str2)  # Hello World

# String Formatting with the % Operator
print("%s %s" % (str1, str2))  # Hello World

# String Formatting with the { } Operators with str.format()
print("{}{}".format(str1, str2))  # Hello World
print("{0}{1}".format(str1, str2))  # Hello World
print("{str1} {str2}".format(str1=str_dict['str1'], str2=str_dict['str2']))  # Hello World
print("{str1} {str2}".format(**str_dict))  # Hello World

# Going From a List to a String in Python With .join()
print(' '.join(str_list))  # Hello World

# Python f'strings --> 3.6 onwards
print(f"{str1} {str2}")  # Hello World

To małe podsumowanie stworzyłem, zamieszczając następujące artykuły.

Kushan Gunasekera
źródło
3

mój przypadek użycia był nieco inny. Musiałem skonstruować zapytanie, w którym ponad 20 pól było dynamicznych. Zastosowałem to podejście przy użyciu metody formatowania

query = "insert into {0}({1},{2},{3}) values({4}, {5}, {6})"
query.format('users','name','age','dna','suzan',1010,'nda')

było to dla mnie stosunkowo prostsze zamiast używania + lub innych sposobów

Ishwar Rimal
źródło