Jak dołączyć jeden ciąg do drugiego w Pythonie?


593

Chcę efektywnego sposobu dołączenia jednego ciągu do drugiego w Pythonie, innego niż poniższe.

var1 = "foo"
var2 = "bar"
var3 = var1 + var2

Czy jest jakaś dobra wbudowana metoda do użycia?


8
TL; DR: Jeśli szukasz prostego sposobu dołączania ciągów, a nie zależy Ci na wydajności:"foo" + "bar" + str(3)
Andrew

Odpowiedzi:


609

Jeśli masz tylko jedno odniesienie do łańcucha i konkatenujesz inny łańcuch do końca, CPython teraz specjalne przypadki to i próbuje przedłużyć łańcuch w miejscu.

W rezultacie operacja jest amortyzowana przez O (n).

na przykład

s = ""
for i in range(n):
    s+=str(i)

kiedyś było O (n ^ 2), ale teraz jest O (n).

Ze źródła (bytesobject.c):

void
PyBytes_ConcatAndDel(register PyObject **pv, register PyObject *w)
{
    PyBytes_Concat(pv, w);
    Py_XDECREF(w);
}


/* The following function breaks the notion that strings are immutable:
   it changes the size of a string.  We get away with this only if there
   is only one module referencing the object.  You can also think of it
   as creating a new string object and destroying the old one, only
   more efficiently.  In any case, don't use this if the string may
   already be known to some other part of the code...
   Note that if there's not enough memory to resize the string, the original
   string object at *pv is deallocated, *pv is set to NULL, an "out of
   memory" exception is set, and -1 is returned.  Else (on success) 0 is
   returned, and the value in *pv may or may not be the same as on input.
   As always, an extra byte is allocated for a trailing \0 byte (newsize
   does *not* include that), and a trailing \0 byte is stored.
*/

int
_PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
{
    register PyObject *v;
    register PyBytesObject *sv;
    v = *pv;
    if (!PyBytes_Check(v) || Py_REFCNT(v) != 1 || newsize < 0) {
        *pv = 0;
        Py_DECREF(v);
        PyErr_BadInternalCall();
        return -1;
    }
    /* XXX UNREF/NEWREF interface should be more symmetrical */
    _Py_DEC_REFTOTAL;
    _Py_ForgetReference(v);
    *pv = (PyObject *)
        PyObject_REALLOC((char *)v, PyBytesObject_SIZE + newsize);
    if (*pv == NULL) {
        PyObject_Del(v);
        PyErr_NoMemory();
        return -1;
    }
    _Py_NewReference(*pv);
    sv = (PyBytesObject *) *pv;
    Py_SIZE(sv) = newsize;
    sv->ob_sval[newsize] = '\0';
    sv->ob_shash = -1;          /* invalidate cached hash value */
    return 0;
}

Łatwo jest to zweryfikować empirycznie.

$ python -m timeit -s "s = ''" "dla i w xrange (10): s + = 'a'"
1000000 pętli, najlepiej 3: 1,85 usec na pętlę
$ python -m timeit -s "s = ''" "dla i w xrange (100): s + = 'a'"
10000 pętli, najlepiej 3: 16,8 użycia na pętlę
$ python -m timeit -s "s = ''" "dla i w xrange (1000): s + = 'a'"
10000 pętli, najlepiej 3: 158 usec na pętlę
$ python -m timeit -s "s = ''" "dla i w xrange (10000): s + = 'a'"
1000 pętli, najlepiej 3: 1,71 ms na pętlę
$ python -m timeit -s "s = ''" "dla i w xrange (100000): s + = 'a'"
10 pętli, najlepiej 3: 14,6 ms na pętlę
$ python -m timeit -s "s = ''" "dla i w xrange (1000000): s + = 'a'"
10 pętli, najlepiej 3: 173 ms na pętlę

Należy jednak pamiętać, że ta optymalizacja nie jest częścią specyfikacji Pythona. O ile wiem, jest to tylko implementacja cPython. Takie same testy empiryczne na przykład pypy lub jython mogą na przykład pokazać starszą wydajność O (n ** 2).

$ pypy -m timeit -s "s = ''" "dla i w xrange (10): s + = 'a'"
10000 pętli, najlepiej 3: 90,8 usec na pętlę
$ pypy -m timeit -s "s = ''" "dla i w xrange (100): s + = 'a'"
1000 pętli, najlepiej 3: 896 usec na pętlę
$ pypy -m timeit -s "s = ''" "dla i w xrange (1000): s + = 'a'"
100 pętli, najlepiej 3: 9,03 ms na pętlę
$ pypy -m timeit -s "s = ''" "dla i w xrange (10000): s + = 'a'"
10 pętli, najlepiej 3: 89,5 ms na pętlę

Jak dotąd tak dobrze, ale potem

$ pypy -m timeit -s "s = ''" "dla i w xrange (100000): s + = 'a'"
10 pętli, najlepiej 3: 12,8 s na pętlę

ouch jest nawet gorszy niż kwadratowy. Więc pypy robi coś, co działa dobrze z krótkimi łańcuchami, ale słabo sprawdza się w przypadku większych łańcuchów.


14
Ciekawy. Przez „teraz” masz na myśli Python 3.x?
Steve Tjoa,

10
@ Steve, Nie. Jest co najmniej w wersji 2.6, a może nawet 2.5
John La Rooy,

8
Cytujesz tę PyString_ConcatAndDelfunkcję, ale dołączasz komentarz do niej _PyString_Resize. Ponadto komentarz tak naprawdę nie potwierdza twojego twierdzenia dotyczącego Big-O
Winston Ewert

3
gratuluję wykorzystania funkcji CPython, która sprawi, że kod będzie indeksował się w innych implementacjach. Zła rada.
Jean-François Fabre

4
NIE używaj tego. Pep8 stwierdza wprost: Kod powinien być napisany w sposób, który nie będzie niekorzystny dla innych implementacji Pythona (PyPy, Jython, IronPython, Cython, Psyco itp.) , A następnie daje ten konkretny przykład jako coś, czego należy unikać, ponieważ jest tak delikatny."".join(str_a, str_b)
Eraw

287

Nie przedwcześnie optymalizuj. Jeśli nie masz powodu, aby sądzić, że istnieje wąskie gardło związane z łączeniem łańcuchów, po prostu trzymaj się +i +=:

s  = 'foo'
s += 'bar'
s += 'baz'

To powiedziawszy, jeśli dążysz do czegoś takiego jak StringBuilder Javy, kanoniczny idiom Pythona polega na dodawaniu elementów do listy, a następnie str.joinłączeniu ich na końcu:

l = []
l.append('foo')
l.append('bar')
l.append('baz')

s = ''.join(l)

Nie wiem, jakie są implikacje szybkości budowania ciągów jako list, a następnie ich .join (), ale uważam, że jest to na ogół najczystszy sposób. Odniosłem również duży sukces przy użyciu notacji% s w ciągu znaków dla silnika szablonów SQL, który napisałem.
richo

25
@Richo Korzystanie z .join jest bardziej wydajne. Powodem jest to, że ciągi Pythona są niezmienne, więc wielokrotne użycie s + = more spowoduje przydzielenie wielu kolejnych większych ciągów. .join wygeneruje końcowy ciąg za jednym razem z jego części składowych.
Ben

5
@Ben, nastąpiła znacząca poprawa w tym obszarze - patrz moja odpowiedź
John La Rooy,

41
str1 = "Hello"
str2 = "World"
newstr = " ".join((str1, str2))

Łączy str1 i str2 ze spacją jako separatory. Możesz też zrobić "".join(str1, str2, ...). str.join()wymaga iteracji, więc musisz umieścić ciągi na liście lub krotce.

Jest to mniej więcej tak wydajne, jak w przypadku wbudowanej metody.


Co się stanie, jeśli str1 jest empy? Czy zostanie ustawiony biały znak?
Jürgen K.,

38

Nie rób

Oznacza to, że w większości przypadków lepiej jest generować cały ciąg za jednym razem, niż dołączać do istniejącego ciągu.

Na przykład nie rób: obj1.name + ":" + str(obj1.count)

Zamiast tego: użyj "%s:%d" % (obj1.name, obj1.count)

To będzie łatwiejsze do odczytania i bardziej wydajne.


54
przykro mi nie ma nic bardziej łatwiejsze do odczytania niż + ciąg (string), jak w pierwszym przykładzie, drugi Przykładem może być bardziej efektywne, ale nie bardziej czytelny
JqueryToAddNumbers

23
@ExceptionSlayer, ciąg + ciąg jest dość łatwy do naśladowania. Ale "<div class='" + className + "' id='" + generateUniqueId() + "'>" + message_text + "</div>"uważam, że mniej czytelny i podatny na błędy"<div class='{classname}' id='{id}'>{message_text}</div>".format(classname=class_name, message_text=message_text, id=generateUniqueId())
Winston Ewert

To wcale nie pomaga, gdy próbuję zrobić z grubsza odpowiednik, powiedzmy, ciąg znaków PHP / perl. = Verifydata () lub podobny.
Shadur

@Shadur, mam na myśli to, że powinieneś pomyśleć jeszcze raz, czy naprawdę chcesz zrobić coś równoważnego, czy może jest to zupełnie inne podejście?
Winston Ewert

1
W tym przypadku odpowiedź na to pytanie brzmi: „Nie, ponieważ to podejście nie obejmuje mojego przypadku użycia”
Shadur

11

Python 3.6 daje nam ciągi F , które są przyjemnością:

var1 = "foo"
var2 = "bar"
var3 = f"{var1}{var2}"
print(var3)                       # prints foobar

W nawiasach klamrowych możesz zrobić wszystko

print(f"1 + 1 == {1 + 1}")        # prints 1 + 1 == 2

10

Jeśli musisz wykonać wiele operacji dołączania, aby zbudować duży ciąg, możesz użyć StringIO lub cStringIO. Interfejs jest jak plik. tj .: możesz writedołączyć do niego tekst.

Jeśli dodajesz tylko dwa ciągi, użyj po prostu +.


9

to naprawdę zależy od twojej aplikacji. Jeśli przeglądasz setki słów i chcesz dołączyć je wszystkie do listy, .join()lepiej. Ale jeśli układasz długie zdanie, lepiej jest użyć +=.


5

Zasadniczo bez różnicy. Jedynym spójnym trendem jest to, że Python wydaje się zwalniać z każdą wersją ... :(


Lista

%%timeit
x = []
for i in range(100000000):  # xrange on Python 2.7
    x.append('a')
x = ''.join(x)

Python 2.7

1 pętla, najlepiej 3: 7,34 s na pętlę

Python 3.4

1 pętla, najlepiej 3: 7,99 s na pętlę

Python 3.5

1 pętla, najlepiej 3: 8,48 s na pętlę

Python 3.6

1 pętla, najlepiej 3: 9,93 s na pętlę


Strunowy

%%timeit
x = ''
for i in range(100000000):  # xrange on Python 2.7
    x += 'a'

Python 2.7 :

1 pętla, najlepiej 3: 7,41 s na pętlę

Python 3.4

1 pętla, najlepiej 3: 9,08 s na pętlę

Python 3.5

1 pętla, najlepiej 3: 8,82 s na pętli

Python 3.6

1 pętla, najlepiej 3: 9,24 s na pętlę


2
Myślę, że to zależy. I dostać 1.19 si 992 msodpowiednio na Python2.7
John La Rooy

4

dołącz ciągi znaków z funkcją __add__

str = "Hello"
str2 = " World"
st = str.__add__(str2)
print(st)

Wynik

Hello World

4
str + str2jest jeszcze krótszy.
Nik O'Lai

2
a='foo'
b='baaz'

a.__add__(b)

out: 'foobaaz'

1
Kod jest fajny, ale pomogłoby to w dołączeniu wyjaśnienia. Dlaczego warto korzystać z tej metody zamiast innych odpowiedzi na tej stronie?
cgmb,

11
Używanie a.__add__(b)jest identyczne jak pisanie a+b. Podczas łączenia ciągów za pomocą +operatora, Python wywoła __add__metodę na ciąg po lewej stronie przechodzącej prawy boczny ciąg jako parametr.
Addie,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.