Jaki jest preferowany sposób łączenia łańcuchów w Pythonie?

Ponieważ Pythona string nie można zmienić, zastanawiałem się, jak efektywniej połączyć łańcuch znaków?

Mogę tak pisać:

s += stringfromelsewhere

Lub w ten sposób:

s = []
s.append(somestring)

later

s = ''.join(s)

Pisząc to pytanie, Znalazłem dobry artykuł mówiący o tym temacie.

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

Ale to w Pythonie 2.x., czyli pytanie czy coś się zmieniło w Pythonie 3?
Author: abhi, 2012-08-29

11 answers

najlepszym sposobem dodawania ciągu znaków do zmiennej łańcuchowej jest użycie + lub +=. To dlatego, że jest czytelny i szybki. Są one również tak samo szybkie, który z nich wybrać jest kwestią gustu, ten ostatni jest najczęściej. Oto timingi z modułem timeit:

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ż dołączanie ciągu znaków do listy jest prawdopodobnie bardzo szybkie w porównaniu z rozszerzaniem sznurek. I to może być prawdą, w niektórych przypadkach. Tutaj, na przykład, jest jeden milion dodanych jednoznakowych łańcuchów, najpierw do łańcucha, a następnie do listy:

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

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

Teraz spróbujmy dodać tysiąc znaków długiego ciągu sto tysięcy razy:

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

Ciąg końcowy, więc kończy się około 100MB długości. To było dość powolne, dodawanie do listy było dużo szybciej. Że ten czas nie obejmuje ostatecznego a.join(). Jak długo to potrwa?

a.join(a):
0.43739795684814453

Oups. Okazuje się, że nawet w tym przypadku dołączanie/dołączanie jest wolniejsze.

Skąd więc bierze się to zalecenie? Python 2?

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

Cóż, append/join jest marginalnie szybsze tam, jeśli używasz bardzo długich łańcuchów (których zazwyczaj nie masz, co byś miał ciąg, który ma 100MB pamięci?)

[[12]}ale prawdziwym klinczerem jest Python 2.3. Gdzie nie pokaże ci nawet czasu, bo jest tak powolny, że jeszcze się nie skończył. Te testy nagle trwają minut . Z wyjątkiem append/join, który jest tak samo szybki jak pod późniejszymi Pythonami.

Yup. Konkatenacja strun była bardzo powolna w Pythonie jeszcze w epoce kamienia. Ale na 2.4 już nie jest (a przynajmniej Python 2.4.7), więc zalecenie używania append/join stało się nieaktualne w 2008 roku, kiedy Python 2.3 przestał być aktualizowany i powinieneś był przestać go używać. :-)

(Update: okazuje się, że kiedy robiłem testy bardziej dokładnie, używanie + i += jest szybsze również dla dwóch ciągów w Pythonie 2.3. Zalecenie stosowania ''.join() musi być nieporozumieniem)

Jednak jest to CPython. Inne implementacje mogą mieć inne obawy. I to jest kolejny powód, dla którego przedwczesna optymalizacja jest źródłem wszelkiego zła. Nie używaj techniki, która ma być "szybsza", chyba że najpierw zmierzysz to.

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

Więc dlaczego używam dużo append/join w moim kodzie? Bo czasami jest wyraźniej. Zwłaszcza, gdy to, co należy połączyć, powinno być oddzielone spacjami, przecinkami lub znakami nowej linii.

 307
Author: Lennart Regebro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-27 01:15:17

Jeśli łączysz wiele wartości, to żadne z nich. Dołączanie listy jest drogie. Możesz użyć do tego StringIO. Zwłaszcza, jeśli budujesz go w ciągu wielu 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ę zwróconą z innej operacji, po prostu użyj ''.join(aList)

Z PYTHONOWEGO FAQ: jaki jest najskuteczniejszy sposób łączenia wielu łańcuchów?

Obiekty Str i bytes są niezmienne, dlatego konkatenacja wielu ciągi razem są nieefektywne, ponieważ każda konkatenacja tworzy nową obiekt. W ogólnym przypadku całkowity koszt wykonania jest kwadratowy w całkowita długość sznurka.

Aby zgromadzić wiele obiektów str, zalecanym idiomem jest umieszczenie ich do listy i wywołania str.join () na końcu:

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

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

Aby zgromadzić wiele obiektów bajtowych, zalecanym idiomem jest rozszerzenie bytearray obiekt wykorzystujący konkatenację in-place (operator+=):

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

Edit: byłem głupi i miałem wyniki wklejone do tyłu, dzięki czemu wyglądało to tak, jakby dodawanie do listy było szybsze niż cStringIO. Dodałem również testy dla bytearray / str concat, a także drugą rundę testów z większą listą z większymi ciągami. (python 2.7.3)

Przykład testu Ipython dla dużych list łańcuchó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
 34
Author: jdi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-25 00:46:00

Zalecaną metodą jest nadal używanie append I join.

 7
Author: MRAB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-29 01:48:02

Stosowanie konkatenacji w miejscu przez ' + ' jest najgorszą metodą konkatenacji pod względem stabilności i implementacji krzyżowej, ponieważ nie obsługuje wszystkich wartości. Standard PEP8 zniechęca do tego i zachęca do używania format(), join() i append() do długotrwałego używania.

 6
Author: badslacks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-08 20:04:59

Jeśli łańcuchy, które konkatenujesz, są literałami, użyj literalnej konkatenacji

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

Jest to przydatne, jeśli chcesz skomentować część łańcucha znaków (jak powyżej) lub jeśli chcesz użyć surowych łańcuchów lub potrójnych cudzysłowów dla części literału, ale nie wszystkich.

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

 6
Author: droid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:43

Choć nieco przestarzały, Kod jak Python: Idiomatic Python poleca join() nad + w tej sekcji . Podobnie jak PythonSpeedPerformanceTips w sekcji string concatenation , z następującym zastrzeżeniem:

[[2]}dokładność tej sekcji jest kwestionowana w odniesieniu do późniejszych wersje Pythona. W CPython 2.5 konkatenacja ciągów jest dość szybko, chociaż może to nie dotyczyć również innych Pythonów wdrożenia. Zobacz też ConcatenationTestCode do dyskusji.
 5
Author: Levon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-29 02:59:03

W Pythonie > = 3.6, nowy łańcuch f jest efektywnym sposobem łączenia łańcuchów.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'
 2
Author: Vikram S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-26 16:46:41

Możesz również użyć tego (bardziej wydajnego). (https://softwareengineering.stackexchange.com/questions/304445/why-is-s-better-than-for-concatenation)

s += "%s" %(stringfromelsewhere)
 1
Author: Vikram S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-12 07:31:21

Piszesz tę funkcję

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

Wtedy możesz zadzwonić gdziekolwiek chcesz

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3
 1
Author: Shameem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-31 06:59:58

Jak wspomina @JDI dokumentacja Pythona sugeruje użycie str.join lub io.StringIO do łączenia łańcuchów. I mówi, że programista powinien oczekiwać kwadratowego czasu od += W pętli, mimo że jest optymalizacja od Pythona 2.4. Jak ta odpowiedź mówi:

Jeśli Python wykryje, że lewy argument nie ma innych referencji, wywoła realloc, aby uniknąć kopii poprzez zmianę rozmiaru łańcucha znaków. To nie jest coś, na czym powinieneś kiedykolwiek polegać, ponieważ jest to szczegóły implementacji i ponieważ jeśli realloc Kończy się koniecznością częstego przesuwania łańcucha, wydajność i tak spada do O(N^2).

Pokażę przykład prawdziwego kodu, który naiwnie opierał się na += tej optymalizacji, ale nie miał zastosowania. Poniższy kod konwertuje powtarzalne krótkie ciągi na większe kawałki, które mają być używane w zbiorczym 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ć godzinami ze względu na kwadratową złożoność czasową. Poniżej znajdują się alternatywy z sugerowanymi danymi struktury:

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

 1
Author: saaj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-04 08:23:38

Mój przypadek użycia był nieco inny. Musiałem skonstruować zapytanie, w którym ponad 20 pól było dynamicznych. Zastosowałem metodę formatowania

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

To było stosunkowo prostsze dla mnie zamiast używać + lub innych sposobów

 0
Author: ishwar rimal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-29 03:33:35