Najbardziej pythonowy sposób na przeplatanie dwóch ciągów

115

Jaki jest najbardziej pytoniczny sposób połączenia dwóch strun?

Na przykład:

Wejście:

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

Wynik:

'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
Brandon Deo
źródło
2
Odpowiedzi tutaj w dużej mierze zakładają, że dwa ciągi wejściowe będą miały tę samą długość. Czy to bezpieczne założenie, czy też potrzebujesz, aby się z tym uporać?
SuperBiasedMan
@SuperBiasedMan Jeśli masz rozwiązanie, warto zobaczyć, jak radzić sobie w każdych warunkach. Ma to znaczenie dla pytania, ale nie w moim przypadku.
Brandon Deo
3
@drexx i tak najlepsza osoba odpowiadająca skomentowała rozwiązanie tego problemu, więc właśnie zredagowałem to w ich poście, aby było wyczerpujące.
SuperBiasedMan

Odpowiedzi:

127

Dla mnie najbardziej pythoniczny * sposób to następujący, który prawie robi to samo, ale używa +operatora do łączenia poszczególnych znaków w każdym ciągu:

res = "".join(i + j for i, j in zip(u, l))
print(res)
# 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

Jest również szybszy niż korzystanie z dwóch join()połączeń:

In [5]: l1 = 'A' * 1000000; l2 = 'a' * 1000000

In [6]: %timeit "".join("".join(item) for item in zip(l1, l2))
1 loops, best of 3: 442 ms per loop

In [7]: %timeit "".join(i + j for i, j in zip(l1, l2))
1 loops, best of 3: 360 ms per loop

Istnieją szybsze metody, ale często zaciemniają one kod.

Uwaga: Jeśli dwa ciągi wejściowe nie mają takiej samej długości, dłuższy zostanie obcięty jako zipzatrzymanie iteracji na końcu krótszego ciągu. W tym przypadku zamiast zipjednego należy użyć zip_longest( izip_longestw Pythonie 2) z itertoolsmodułu, aby upewnić się, że oba ciągi są całkowicie wyczerpane.


* Cytat z Zen of Python : liczy się czytelność .
Pythonic = czytelność dla mnie; i + jjest po prostu łatwiej analizowany wizualnie, przynajmniej dla moich oczu.

Dimitris Fasarakis Hilliard
źródło
1
Jednak wysiłek związany z kodowaniem n łańcuchów to O (n). Mimo to jest dobre, o ile n jest małe.
TigerhawkT3
Twój generator prawdopodobnie powoduje większe obciążenie niż połączenie.
Padraic Cunningham
5
biegnie "".join([i + j for i, j in zip(l1, l2)])i na pewno będzie najszybszy
Padraic Cunningham
6
"".join(map("".join, zip(l1, l2)))jest jeszcze szybszy, choć niekoniecznie bardziej pythonowy.
Aleksi Torhamo
63

Szybsza alternatywa

Inny sposób:

res = [''] * len(u) * 2
res[::2] = u
res[1::2] = l
print(''.join(res))

Wynik:

'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

Prędkość

Wygląda na to, że jest szybszy:

%%timeit
res = [''] * len(u) * 2
res[::2] = u
res[1::2] = l
''.join(res)

100000 loops, best of 3: 4.75 µs per loop

niż najszybsze dotychczas rozwiązanie:

%timeit "".join(list(chain.from_iterable(zip(u, l))))

100000 loops, best of 3: 6.52 µs per loop

Również w przypadku większych strun:

l1 = 'A' * 1000000; l2 = 'a' * 1000000

%timeit "".join(list(chain.from_iterable(zip(l1, l2))))
1 loops, best of 3: 151 ms per loop


%%timeit
res = [''] * len(l1) * 2
res[::2] = l1
res[1::2] = l2
''.join(res)

10 loops, best of 3: 92 ms per loop

Python 3.5.1.

Wariacja dla strun o różnych długościach

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijkl'

Krótszy określa długość ( zip()ekwiwalent)

min_len = min(len(u), len(l))
res = [''] * min_len * 2 
res[::2] = u[:min_len]
res[1::2] = l[:min_len]
print(''.join(res))

Wynik:

AaBbCcDdEeFfGgHhIiJjKkLl

Dłuższy określa długość ( itertools.zip_longest(fillvalue='')ekwiwalent)

min_len = min(len(u), len(l))
res = [''] * min_len * 2 
res[::2] = u[:min_len]
res[1::2] = l[:min_len]
res += u[min_len:] + l[min_len:]
print(''.join(res))

Wynik:

AaBbCcDdEeFfGgHhIiJjKkLlMNOPQRSTUVWXYZ
Mike Müller
źródło
49

Z join()i zip().

>>> ''.join(''.join(item) for item in zip(u,l))
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
TigerhawkT3
źródło
17
Lub''.join(itertools.chain.from_iterable(zip(u, l)))
Blender
1
Spowoduje to obcięcie listy, jeśli jedna jest krótsza od drugiej, jak również zipzatrzymanie, gdy krótsza lista została w pełni iterowana.
SuperBiasedMan
5
@SuperBiasedMan - Tak. itertools.zip_longestmożna użyć, jeśli stanie się to problemem.
TigerhawkT3
18

W Pythonie 2, zdecydowanie szybszym sposobem robienia rzeczy, przy ~ 3-krotnej szybkości cięcia list dla małych łańcuchów i ~ 30-krotnie dla długich, jest

res = bytearray(len(u) * 2)
res[::2] = u
res[1::2] = l
str(res)

To jednak nie zadziała w Pythonie 3. Możesz zaimplementować coś takiego

res = bytearray(len(u) * 2)
res[::2] = u.encode("ascii")
res[1::2] = l.encode("ascii")
res.decode("ascii")

ale do tego czasu straciłeś już korzyści wynikające z krojenia list dla małych ciągów (wciąż jest to 20 razy szybsze dla długich łańcuchów), a to jeszcze nie działa nawet dla znaków spoza ASCII.

FWIW, jeśli w ten sposób na masywne ciągi i trzeba w każdym cyklu, a z jakiegoś powodu trzeba użyć ciągi Pythona ... oto jak to zrobić:

res = bytearray(len(u) * 4 * 2)

u_utf32 = u.encode("utf_32_be")
res[0::8] = u_utf32[0::4]
res[1::8] = u_utf32[1::4]
res[2::8] = u_utf32[2::4]
res[3::8] = u_utf32[3::4]

l_utf32 = l.encode("utf_32_be")
res[4::8] = l_utf32[0::4]
res[5::8] = l_utf32[1::4]
res[6::8] = l_utf32[2::4]
res[7::8] = l_utf32[3::4]

res.decode("utf_32_be")

Pomoże też w tym specjalna obudowa, stosowana w zwykłym przypadku mniejszych typów. FWIW, jest to tylko 3-krotnie szybsze cięcie list dla długich ciągów i współczynnik 4 do 5 wolniejsze dla małych.

Tak czy inaczej wolę joinrozwiązania, ale skoro w innym miejscu wspomniano o czasach , pomyślałem, że równie dobrze mogę się przyłączyć.

Veedrac
źródło
16

Jeśli chcesz najszybszy sposób, możesz połączyć itertools z operator.add:

In [36]: from operator import add

In [37]: from itertools import  starmap, izip

In [38]: timeit "".join([i + j for i, j in uzip(l1, l2)])
1 loops, best of 3: 142 ms per loop

In [39]: timeit "".join(starmap(add, izip(l1,l2)))
1 loops, best of 3: 117 ms per loop

In [40]: timeit "".join(["".join(item) for item in zip(l1, l2)])
1 loops, best of 3: 196 ms per loop

In [41]:  "".join(starmap(add, izip(l1,l2))) ==  "".join([i + j   for i, j in izip(l1, l2)]) ==  "".join(["".join(item) for item in izip(l1, l2)])
Out[42]: True

Ale łączenie izipi chain.from_iterableznowu jest szybsze

In [2]: from itertools import  chain, izip

In [3]: timeit "".join(chain.from_iterable(izip(l1, l2)))
10 loops, best of 3: 98.7 ms per loop

Istnieje również zasadnicza różnica między chain(*i chain.from_iterable(....

In [5]: timeit "".join(chain(*izip(l1, l2)))
1 loops, best of 3: 212 ms per loop

Nie ma czegoś takiego jak generator z łączeniem, przepuszczenie jednego będzie zawsze wolniejsze, ponieważ Python najpierw zbuduje listę przy użyciu zawartości, ponieważ wykonuje dwa przejścia przez dane, jeden do określenia potrzebnego rozmiaru, a drugi do faktycznego wykonania połączenie, które nie byłoby możliwe przy użyciu generatora:

join.h :

 /* Here is the general case.  Do a pre-pass to figure out the total
  * amount of space we'll need (sz), and see whether all arguments are
  * bytes-like.
   */

Również jeśli masz ciągi o innej długości i nie chcesz stracić danych, możesz użyć izip_longest :

In [22]: from itertools import izip_longest    
In [23]: a,b = "hlo","elworld"

In [24]:  "".join(chain.from_iterable(izip_longest(a, b,fillvalue="")))
Out[24]: 'helloworld'

W przypadku Pythona 3 nazywa się to zip_longest

Ale w przypadku Pythona2 sugestia Veedrac jest zdecydowanie najszybsza:

In [18]: %%timeit
res = bytearray(len(u) * 2)
res[::2] = u
res[1::2] = l
str(res)
   ....: 
100 loops, best of 3: 2.68 ms per loop
Padraic Cunningham
źródło
2
dlaczego list?? jest niepotrzebne
Copperfield
1
nie według moich testów, tracisz czas na tworzenie listy pośredniej, a to mija się z celem używania iteratorów. Timeit the "".join(list(...))give me 6.715280318699769 and timeit the "".join(starmap(...))give me 6.46332361384313
Copperfield
1
co w takim razie jest zależne od maszyny? ponieważ bez względu na to, gdzie przeprowadzam test, otrzymuję ten sam dokładny wynik "".join(list(starmap(add, izip(l1,l2))))jest wolniejszy niż "".join(starmap(add, izip(l1,l2))). Uruchamiam test na moim komputerze w pythonie 2.7.11 i pythonie 3.5.1 nawet w wirtualnej konsoli www.python.org z pythonem 3.4.3 i wszyscy mówią to samo i uruchamiam go kilka razy i zawsze to samo
Copperfield
Czytałem i widzę, że buduje wewnętrznie listę cały czas w swoich buforach zmienną niezależnie od tego, co do niej przekazujesz, więc tym bardziej dlaczego NIE daj jej listy
Copperfield
@Copperfield, czy mówisz o wywołaniu listy czy o przekazaniu listy?
Padraic Cunningham
12

Możesz to również zrobić za pomocą mapi operator.add:

from operator import add

u = 'AAAAA'
l = 'aaaaa'

s = "".join(map(add, u, l))

Wyjście :

'AaAaAaAaAa'

To, co robi mapa, to pobiera każdy element z pierwszej iterowalnej ui pierwsze elementy z drugiej iterowalnej li stosuje funkcję dostarczoną jako pierwszy argument add. Następnie dołącz po prostu dołącz do nich.

korzeń
źródło
9

Odpowiedź Jima jest świetna, ale oto moja ulubiona opcja, jeśli nie masz nic przeciwko kilku importom:

from functools import reduce
from operator import add

reduce(add, map(add, u, l))
dzianina
źródło
7
Powiedział, że większość Pythonic, nie większość Haskellic;)
Curt
7

Wiele z tych sugestii zakłada, że ​​struny mają jednakową długość. Może to obejmuje wszystkie rozsądne przypadki użycia, ale przynajmniej wydaje mi się, że możesz chcieć pomieścić również struny o różnych długościach. A może tylko ja uważam, że siatka powinna działać trochę tak:

u = "foobar"
l = "baz"
mesh(u,l) = "fboaozbar"

Można to zrobić w następujący sposób:

def mesh(a,b):
    minlen = min(len(a),len(b))
    return "".join(["".join(x+y for x,y in zip(a,b)),a[minlen:],b[minlen:]])
Christofer Ohlsson
źródło
5

Lubię używać dwóch for, nazwy zmiennych mogą dać wskazówkę / przypomnienie o tym, co się dzieje:

"".join(char for pair in zip(u,l) for char in pair)
Neal Fultz
źródło
4

Aby dodać kolejne, bardziej podstawowe podejście:

st = ""
for char in u:
    st = "{0}{1}{2}".format( st, char, l[ u.index( char ) ] )
WeRelic
źródło
4

Trochę nie-pythonicznie wydaje się nie brać pod uwagę odpowiedzi na podwójne rozumienie listy tutaj, aby obsłużyć n string z wysiłkiem O (1):

"".join(c for cs in itertools.zip_longest(*all_strings) for c in cs)

gdzie all_stringsjest lista ciągów, które chcesz przeplatać. W twoim przypadku all_strings = [u, l]. Pełny przykład użycia wyglądałby następująco:

import itertools
a = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b = 'abcdefghijklmnopqrstuvwxyz'
all_strings = [a,b]
interleaved = "".join(c for cs in itertools.zip_longest(*all_strings) for c in cs)
print(interleaved)
# 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

Jak wiele odpowiedzi, najszybciej? Prawdopodobnie nie, ale proste i elastyczne. Ponadto, bez zbyt dużej złożoności, jest to nieco szybsze niż akceptowana odpowiedź (ogólnie dodawanie ciągów jest nieco powolne w Pythonie):

In [7]: l1 = 'A' * 1000000; l2 = 'a' * 1000000;

In [8]: %timeit "".join(a + b for i, j in zip(l1, l2))
1 loops, best of 3: 227 ms per loop

In [9]: %timeit "".join(c for cs in zip(*(l1, l2)) for c in cs)
1 loops, best of 3: 198 ms per loop
scnerd
źródło
Jednak nadal nie jest tak szybka, jak najszybsza odpowiedź: co dostało 50,3 ms na tych samych danych i komputerze
scnerd
3

Potencjalnie szybsze i krótsze niż obecne wiodące rozwiązanie:

from itertools import chain

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

res = "".join(chain(*zip(u, l)))

Jeśli chodzi o szybkość strategii, należy zrobić jak najwięcej na poziomie C. Ta sama poprawka zip_longest () dla nierównych łańcuchów i wychodziłaby z tego samego modułu co chain (), więc nie mogę tam dać mi zbyt wielu punktów!

Inne rozwiązania, które wymyśliłem po drodze:

res = "".join(u[x] + l[x] for x in range(len(u)))

res = "".join(k + l[i] for i, k in enumerate(u))
cdlane
źródło
3

Możesz użyć 1iteration_utilities.roundrobin

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

from iteration_utilities import roundrobin
''.join(roundrobin(u, l))
# returns 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

lub ManyIterablesklasa z tego samego pakietu:

from iteration_utilities import ManyIterables
ManyIterables(u, l).roundrobin().as_string()
# returns 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

1 Jest to z biblioteki trzeciej mam napisane: iteration_utilities.

MSeifert
źródło
2

Użyłbym zip (), aby uzyskać czytelny i łatwy sposób:

result = ''
for cha, chb in zip(u, l):
    result += '%s%s' % (cha, chb)

print result
# 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
valeas
źródło