Czy jest jakiś powód, aby nie używać znaku „+” do łączenia dwóch ciągów?

124

Typowym antywzorem w Pythonie jest konkatenacja sekwencji ciągów za +pomocą pętli. Jest to złe, ponieważ interpreter Pythona musi utworzyć nowy obiekt łańcuchowy dla każdej iteracji, co w rezultacie zajmuje czas kwadratowy. (Najnowsze wersje CPythona mogą najwyraźniej optymalizować to w niektórych przypadkach, ale inne implementacje nie mogą, więc programistów odradza się polegać na tym.) ''.joinJest właściwym sposobem zrobienia tego.

Jednak słyszałem, jak powiedziano (w tym tutaj na Stack Overflow ), że nigdy, przenigdy nie powinieneś używać +do łączenia ciągów, ale zamiast tego zawsze używaj ''.joinlub ciągu formatującego. Nie rozumiem, dlaczego tak się dzieje, jeśli łączysz tylko dwa ciągi. Jeśli moje rozumienie jest poprawne, nie powinno to zająć czasu kwadratowego i myślę, że a + bjest bardziej przejrzyste i czytelne niż jedno ''.join((a, b))lub drugie '%s%s' % (a, b).

Czy jest dobrą praktyką +łączenie dwóch ciągów? A może jest problem, którego nie jestem świadomy?

Taymon
źródło
Jest schludniejszy i masz większą kontrolę, aby nie wykonywać konkatenacji. ALE jego nieco wolniejszy, uderzający w struny kompromis: P
Jakob Bowyer
Mówisz, że +jest szybszy czy wolniejszy? I dlaczego?
Taymon
1
+ jest szybszy, In [2]: %timeit "a"*80 + "b"*80 1000000 loops, best of 3: 356 ns per loop In [3]: %timeit "%s%s" % ("a"*80, "b"*80) 1000000 loops, best of 3: 907 ns per loop
Jakob Bowyer
4
In [3]: %timeit "%s%s" % (a, b) 1000000 loops, best of 3: 590 ns per loop In [4]: %timeit a + b 10000000 loops, best of 3: 147 ns per loop
Jakob Bowyer
1
@JakobBowyer i inni: Argument „konkatenacja ciągów jest zła” nie ma prawie nic wspólnego z szybkością, ale korzysta z automatycznej konwersji typów z __str__. Zobacz moją odpowiedź na przykłady.
Izkata,

Odpowiedzi:

120

Nie ma nic złego w łączeniu dwóch ciągów z +. Rzeczywiście, łatwiej to czytać niż ''.join([a, b]).

Masz jednak rację, że łączenie więcej niż 2 ciągów za pomocą +jest operacją O (n ^ 2) (w porównaniu do O (n) for join) i przez to staje się nieefektywne. Nie ma to jednak nic wspólnego z używaniem pętli. Parzystość a + b + c + ...to O (n ^ 2), a powodem jest to, że każda konkatenacja tworzy nowy ciąg.

CPython2.4 i nowsze starają się to złagodzić, ale nadal zaleca się używanie joinpodczas łączenia więcej niż 2 ciągów.

ggozad
źródło
5
@Mutant: .joinprzyjmuje iterowalne , więc oba .join([a,b])i .join((a,b))są poprawne.
odnalezienie
1
Ciekawe czasy wskazują na użycie +lub +=w akceptowanej odpowiedzi (od 2013 r.) Na stackoverflow.com/a/12171382/378826 (z Lennart Regebro) nawet dla CPythona 2.3+ i wybranie wzorca „dołącz / dołącz” tylko wtedy, gdy ten jaśniej ujawnia pomysł na rozwiązanie problemu.
Dilettant
49

Operator Plus jest idealnym rozwiązaniem do łączenia dwóch ciągów języka Python. Ale jeśli będziesz dodawać więcej niż dwa ciągi (n> 25), możesz pomyśleć o czymś innym.

''.join([a, b, c]) sztuczka polega na optymalizacji wydajności.

Mikko Ohtamaa
źródło
2
Czy krotka nie byłaby lepsza niż lista?
ThiefMaster
7
Krotka byłaby szybsza - kod był tylko przykładem :) Zwykle długie ciągi wejściowe są dynamiczne.
Mikko Ohtamaa
5
@martineau Myślę, że ma na myśli dynamiczne generowanie i wstawianie append()łańcuchów do listy.
Peter C
5
Muszę tu powiedzieć: krotka ma zwykle POWOLNĄ strukturę, zwłaszcza jeśli rośnie. Z list możesz używać list.extend (list_of_items) i list.append (item), które są znacznie szybsze podczas dynamicznego łączenia rzeczy.
Antti Haapala,
6
+1 dla n > 25. Ludzie potrzebują punktów odniesienia, aby od czegoś zacząć.
n611x007
8

Założenie, że nigdy, przenigdy nie powinno się używać znaku + do konkatenacji ciągów, ale zamiast tego zawsze używać '' .join, może być mitem. Prawdą jest, że using +tworzy niepotrzebne tymczasowe kopie niezmiennego obiektu string, ale innym nieczęsto cytowanym faktem jest to, że wywołanie joinpętli generalnie spowodowałoby dodanie narzutu function call. Weźmy przykład.

Utwórz dwie listy, jedną z połączonego pytania SO, a drugą, większą, utworzoną

>>> myl1 = ['A','B','C','D','E','F']
>>> myl2=[chr(random.randint(65,90)) for i in range(0,10000)]

Pozwala tworzyć dwie funkcje, UseJoini UsePlusużywać odpowiedni joini +funkcjonalność.

>>> def UsePlus():
    return [myl[i] + myl[i + 1] for i in range(0,len(myl), 2)]

>>> def UseJoin():
    [''.join((myl[i],myl[i + 1])) for i in range(0,len(myl), 2)]

Pozwala uruchomić timeit z pierwszą listą

>>> myl=myl1
>>> t1=timeit.Timer("UsePlus()","from __main__ import UsePlus")
>>> t2=timeit.Timer("UseJoin()","from __main__ import UseJoin")
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=100000)/100000)
2.48 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=100000)/100000)
2.61 usec/pass
>>> 

Mają prawie taki sam czas działania.

Użyjmy cProfile

>>> myl=myl2
>>> cProfile.run("UsePlus()")
         5 function calls in 0.001 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <pyshell#1376>:1(UsePlus)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("UseJoin()")
         5005 function calls in 0.029 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.015    0.015    0.029    0.029 <pyshell#1388>:1(UseJoin)
        1    0.000    0.000    0.029    0.029 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     5000    0.014    0.000    0.014    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {range}

Wygląda na to, że użycie Join powoduje niepotrzebne wywołania funkcji, które mogą zwiększyć narzut.

A teraz wracając do pytania. Czy we wszystkich przypadkach należy zniechęcać do używania +over join?

Uważam, że nie, należy wziąć pod uwagę

  1. Długość rozpatrywanego łańcucha
  2. Liczba operacji konkatenacji.

A poza tym w rozwoju przedwczesnej optymalizacji jest zła.

Abhijit
źródło
7
Oczywiście, idea polegałaby na tym, aby nie używać joinwewnątrz samej pętli - raczej pętla generowałaby sekwencję, która byłaby przekazywana do połączenia.
jsbueno
7

Podczas pracy z wieloma osobami czasami trudno jest dokładnie wiedzieć, co się dzieje. Użycie ciągu formatu zamiast konkatenacji może uniknąć jednej konkretnej irytacji, która zdarzała się nam mnóstwo razy:

Powiedzmy, funkcja wymaga argumentu i piszesz ją, oczekując otrzymania łańcucha:

In [1]: def foo(zeta):
   ...:     print 'bar: ' + zeta

In [2]: foo('bang')
bar: bang

Tak więc ta funkcja może być używana dość często w całym kodzie. Twoi współpracownicy mogą dokładnie wiedzieć, co robi, ale niekoniecznie są w pełni na bieżąco z elementami wewnętrznymi i mogą nie wiedzieć, że funkcja oczekuje łańcucha. I tak mogą skończyć się tym:

In [3]: foo(23)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

/home/izkata/<ipython console> in foo(zeta)

TypeError: cannot concatenate 'str' and 'int' objects

Nie byłoby problemu, gdybyś użył tylko ciągu formatu:

In [1]: def foo(zeta):
   ...:     print 'bar: %s' % zeta
   ...:     
   ...:     

In [2]: foo('bang')
bar: bang

In [3]: foo(23)
bar: 23

To samo dotyczy wszystkich typów obiektów, które definiują __str__, które również można przekazać:

In [1]: from datetime import date

In [2]: zeta = date(2012, 4, 15)

In [3]: print 'bar: ' + zeta
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

TypeError: cannot concatenate 'str' and 'datetime.date' objects

In [4]: print 'bar: %s' % zeta
bar: 2012-04-15

Więc tak: jeśli możesz użyć ciągu formatu, zrób to i wykorzystaj to, co ma do zaoferowania Python.

Izkata
źródło
1
+1 za dobrze uzasadnioną opinię odrębną. Nadal uważam, że jestem za +choć.
Taymon
1
Dlaczego po prostu nie zdefiniowałbyś metody foo jako: print 'bar:' + str (zeta)?
EngineerWithJava54321
@ EngineerWithJava54321 Na przykład zeta = u"a\xac\u1234\u20ac\U00008000"- musisz użyć, print 'bar: ' + unicode(zeta)aby upewnić się, że nie zawiera błędów. %srobi to dobrze, nie myśląc o tym, i jest znacznie krótszy
Izkata
@ EngineerWithJava54321 Inne przykłady są tutaj mniej istotne, ale na przykład "bar: %s"mogą być przetłumaczone na "zrb: %s br"inny język. %sWersja będzie tylko praca, ale wersja string-concat stałaby się bałagan obsłużyć wszystkie przypadki i tłumacze będą teraz dwa oddzielne tłumaczenia do czynienia z
Izkata
Jeśli nie wiedzą, jaka jest implementacja foo, napotkają ten błąd z dowolnym def.
wnętrze we wrześniu
3

Zrobiłem szybki test:

import sys

str = e = "a xxxxxxxxxx very xxxxxxxxxx long xxxxxxxxxx string xxxxxxxxxx\n"

for i in range(int(sys.argv[1])):
    str = str + e

i zaplanowałem to:

mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  8000000
8000000 times

real    0m2.165s
user    0m1.620s
sys     0m0.540s
mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  16000000
16000000 times

real    0m4.360s
user    0m3.480s
sys     0m0.870s

Najwyraźniej istnieje optymalizacja dla a = a + bprzypadku. Nie wykazuje czasu O (n ^ 2), jak można by przypuszczać.

Tak więc, przynajmniej pod względem wydajności, używanie +jest w porządku.

Michael Slade
źródło
3
Możesz porównać to do przypadku „dołączenia” tutaj. I jest kwestia innych implementacji Pythona, takich jak pypy, jython, ironpython itp ...
jsbueno,
3

Według dokumentacji Pythona, użycie str.join () zapewni spójność wydajności w różnych implementacjach Pythona. Chociaż CPython optymalizuje kwadratowe zachowanie s = s + t, inne implementacje Pythona mogą tego nie robić.

Szczegóły implementacji CPythona : jeśli s i t są ciągami znaków, niektóre implementacje Pythona, takie jak CPython, mogą zwykle przeprowadzać optymalizację w miejscu przypisań w postaci s = s + t lub s + = t. Gdy ma to zastosowanie, ta optymalizacja znacznie zmniejsza prawdopodobieństwo wykonania kwadratowego czasu pracy. Ta optymalizacja jest zależna zarówno od wersji, jak i implementacji. W przypadku kodu wrażliwego na wydajność zaleca się użycie metody str.join (), która zapewnia spójną wydajność liniowego konkatenacji między wersjami i implementacjami.

Typy sekwencji w dokumentach Pythona (zobacz przypis [6])

Książę
źródło
2

Używam następujących z Pythonem 3.8

string4 = f'{string1}{string2}{string3}'
Lucas Vazquez
źródło
0

'' .join ([a, b]) jest lepszym rozwiązaniem niż + .

Ponieważ kod powinien być napisany w sposób, który nie szkodzi innym implementacjom Pythona (PyPy, Jython, IronPython, Cython, Psyco i tym podobnych)

form a + = b lub a = a + b jest krucha nawet w CPythonie i nie występuje w ogóle w implementacjach , które nie używają refcounting (liczenie referencji to technika przechowywania liczby referencji, wskaźników lub uchwytów do a zasób, taki jak obiekt, blok pamięci, miejsce na dysku lub inny zasób )

https://www.python.org/dev/peps/pep-0008/#programming-recommendations

muhammad ali e
źródło
1
a += bdziała we wszystkich implementacjach Pythona, po prostu na niektórych z nich zajmuje to kwadratowy czas, gdy wykonuje się je wewnątrz pętli ; pytanie dotyczyło konkatenacji ciągów poza pętlą.
Taymon