Maneira mais pythônica de intercalar duas cordas

115

Qual é a maneira mais pythônica de unir duas cordas?

Por exemplo:

Entrada:

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

Resultado:

'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
Brandon Deo
fonte
2
As respostas aqui assumiram amplamente que suas duas strings de entrada terão o mesmo comprimento. É uma suposição segura ou você precisa que isso seja resolvido?
SuperBisedMan
@SuperBIASMan Pode ser útil ver como lidar com todas as condições se você tiver uma solução. É relevante para a pergunta, mas não especificamente para o meu caso.
Brandon Deo
3
@drexx O principal respondedor comentou com uma solução para isso de qualquer maneira, então eu apenas editei em sua postagem para que seja abrangente.
SuperBisedMan

Respostas:

127

Para mim, a maneira mais pythônica * é a seguinte, que faz praticamente a mesma coisa, mas usa o +operador para concatenar os caracteres individuais em cada string:

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

Também é mais rápido do que usar duas join()chamadas:

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

Existem abordagens mais rápidas, mas muitas vezes ofuscam o código.

Nota: Se as duas strings de entrada não tiverem o mesmo comprimento, a mais longa será truncada, pois zippara de iterar no final da string mais curta. Neste caso, em vez de zipum, deve-se usar zip_longest( izip_longestno Python 2) do itertoolsmódulo para garantir que ambas as strings estejam totalmente esgotadas.


* Para fazer uma citação do Zen do Python : Contagens de legibilidade .
Pythônico = legibilidade para mim; i + jé apenas analisado visualmente com mais facilidade, pelo menos para os meus olhos.

Dimitris Fasarakis Hilliard
fonte
1
Entretanto, o esforço de codificação para n strings é O (n). Ainda assim, é bom, desde que n seja pequeno.
TigerhawkT3
Seu gerador provavelmente está causando mais sobrecarga do que a junção.
Padraic Cunningham
5
correr "".join([i + j for i, j in zip(l1, l2)])e será definitivamente o mais rápido
Padraic Cunningham
6
"".join(map("".join, zip(l1, l2)))é ainda mais rápido, embora não necessariamente mais python.
Aleksi Torhamo
63

Alternativa mais rápida

Outra maneira:

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

Resultado:

'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

Rapidez

Parece que é mais rápido:

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

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

do que a solução mais rápida até agora:

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

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

Também para strings maiores:

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.

Variação para cordas com diferentes comprimentos

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijkl'

O mais curto determina o comprimento ( zip()equivalente)

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))

Resultado:

AaBbCcDdEeFfGgHhIiJjKkLl

O mais longo determina o comprimento ( itertools.zip_longest(fillvalue='')equivalente)

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))

Resultado:

AaBbCcDdEeFfGgHhIiJjKkLlMNOPQRSTUVWXYZ
Mike Müller
fonte
49

Com join()e zip().

>>> ''.join(''.join(item) for item in zip(u,l))
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
TigerhawkT3
fonte
17
Ou''.join(itertools.chain.from_iterable(zip(u, l)))
Blender
1
Isso truncará uma lista se uma for menor do que a outra, pois zippara quando a lista menor tiver sido totalmente iterada.
SuperBastedMan
5
@SuperBastedMan - Sim. itertools.zip_longestpode ser usado se se tornar um problema.
TigerhawkT3
18

No Python 2, de longe a maneira mais rápida de fazer as coisas, em ~ 3x a velocidade do fatiamento de lista para strings pequenas e ~ 30x para strings longas, é

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

Isso não funcionaria no Python 3, no entanto. Você poderia implementar algo como

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

mas, a essa altura, você já perdeu os ganhos com o fatiamento de lista para strings pequenas (ainda é 20x a velocidade para strings longas) e isso nem funciona para caracteres não ASCII ainda.

FWIW, se você está fazendo isso em strings massivas e precisa de cada ciclo e , por algum motivo, precisa usar strings Python ... veja como fazer:

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")

A embalagem especial, o caso comum de tipos menores, também ajudará. FWIW, esta é apenas 3x a velocidade de corte de lista para cordas longas e um fator de 4 a 5 mais lento para cordas pequenas.

De qualquer forma, prefiro as joinsoluções, mas como os horários foram mencionados em outro lugar, achei melhor participar.

Veedrac
fonte
16

Se você quiser a maneira mais rápida, pode combinar itertools com 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

Mas combinar izipe chain.from_iterableé mais rápido novamente

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

Também há uma diferença substancial entre chain(*e chain.from_iterable(....

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

Não existe gerador com junção, a passagem de um sempre será mais lenta, pois o python primeiro construirá uma lista usando o conteúdo porque faz duas passagens sobre os dados, uma para descobrir o tamanho necessário e outra para realmente fazer a junção que não seria possível usando um gerador:

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.
   */

Além disso, se você tiver strings de comprimento diferente e não quiser perder dados, pode usar 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'

Para python 3 é chamado zip_longest

Mas para python2, a sugestão de veedrac é de longe a mais rápida:

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
fonte
2
porque list?? é desnecessário
Copperfield
1
não de acordo com meus testes, você perde tempo fazendo a lista de intermediários e isso invalida o propósito de usar iteradores. Timeit "".join(list(...))para me dar 6,715280318699769 e timeit "".join(starmap(...))para me dar 6,46332361384313
Copperfield
1
então o quê, é dependente da máquina? porque não importa onde eu execute o teste, obtenho o mesmo resultado exato, "".join(list(starmap(add, izip(l1,l2))))é mais lento do que "".join(starmap(add, izip(l1,l2))). Eu executo o teste em minha máquina em python 2.7.11 e em python 3.5.1 até mesmo no console virtual de www.python.org com python 3.4.3 e todos dizem o mesmo e eu o executo algumas vezes e sempre o mesmo
Copperfield
Eu li e o que vejo é que ele constrói uma lista internamente o tempo todo em sua variável de buffers independente do que você passa para ela, então mais razão para NÃO dar uma lista
Copperfield
@Copperfield, você está falando sobre a chamada de lista ou está passando uma lista?
Padraic Cunningham
12

Você também pode fazer isso usando mape operator.add:

from operator import add

u = 'AAAAA'
l = 'aaaaa'

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

Produto :

'AaAaAaAaAa'

O que o mapa faz é pegar cada elemento do primeiro iterável ue os primeiros elementos do segundo iterável le aplicar a função fornecida como o primeiro argumento add. Em seguida, junte-se a eles.

raiz
fonte
9

A resposta de Jim é ótima, mas aqui está minha opção favorita, se você não se importar com algumas importações:

from functools import reduce
from operator import add

reduce(add, map(add, u, l))
malha
fonte
7
Ele disse mais pitônico, não mais haskélico;)
Curt
7

Muitas dessas sugestões assumem que as strings têm o mesmo comprimento. Talvez isso cubra todos os casos de uso razoáveis, mas pelo menos para mim parece que você pode querer acomodar strings de comprimentos diferentes também. Ou eu sou o único pensando que a malha deveria funcionar um pouco assim:

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

Uma maneira de fazer isso seria a seguinte:

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
fonte
5

Eu gosto de usar dois fors, os nomes das variáveis ​​podem dar uma dica / lembrete do que está acontecendo:

"".join(char for pair in zip(u,l) for char in pair)
Neal Fultz
fonte
4

Apenas para adicionar outra abordagem mais básica:

st = ""
for char in u:
    st = "{0}{1}{2}".format( st, char, l[ u.index( char ) ] )
WeRelic
fonte
4

Parece um pouco não-pythônico não considerar a resposta de compreensão de lista dupla aqui, para lidar com n strings com esforço O (1):

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

onde all_stringsestá uma lista das strings que você deseja intercalar. No seu caso all_strings = [u, l],. Um exemplo de uso completo ficaria assim:

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'

Como muitas respostas, mais rápido? Provavelmente não, mas simples e flexível. Além disso, sem adicionar muita complexidade, isso é um pouco mais rápido do que a resposta aceita (em geral, a adição de string é um pouco lenta em python):

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
fonte
Ainda não tão rápido quanto a resposta mais rápida: que obteve 50,3 ms
nesses
3

Potencialmente mais rápido e mais curto do que a solução líder atual:

from itertools import chain

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

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

A estratégia em termos de velocidade é fazer o máximo possível no nível C. A mesma correção zip_longest () para strings desiguais e ela sairia do mesmo módulo que chain (), portanto, não pode me dar muitos pontos aí!

Outras soluções que encontrei ao longo do caminho:

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

res = "".join(k + l[i] for i, k in enumerate(u))
cdlane
fonte
3

Você poderia usar 1iteration_utilities.roundrobin

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

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

ou a ManyIterablesclasse do mesmo pacote:

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

1 Esta é a partir de uma biblioteca de terceiros que tenho escrito: iteration_utilities.

MSeifert
fonte
2

Eu usaria zip () para obter uma maneira fácil e legível:

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

print result
# 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
Valeas
fonte