Самый питонический способ перемежать две строки

115

Какой самый питонический способ соединить две строки вместе?

Например:

Входные данные:

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

Вывод:

'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
Брэндон Део
источник
2
Ответы здесь в основном предполагали, что две ваши входные строки будут одинаковой длины. Это безопасное предположение или вам нужно с этим справиться?
SuperBiasedMan
@SuperBiasedMan Если у вас есть решение, может быть полезно посмотреть, как справиться со всеми условиями. Это относится к вопросу, но не ко мне конкретно.
Brandon Deo,
3
@drexx Главный ответчик так или иначе прокомментировал решение этой проблемы, поэтому я просто отредактировал его в своем посте, чтобы он был исчерпывающим.
SuperBiasedMan

Ответы:

127

Для меня наиболее питоническим * способом является следующий, который в значительной степени делает то же самое, но использует +оператор для объединения отдельных символов в каждой строке:

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

Это также быстрее, чем использование двух join()вызовов:

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

Существуют более быстрые подходы, но они часто запутывают код.

Примечание. Если две входные строки не одинаковой длины, более длинная будет усечена, так как zipперебор прекращается в конце более короткой строки. В этом случае вместо zipодного следует использовать zip_longest( izip_longestв Python 2) из itertoolsмодуля, чтобы убедиться, что обе строки полностью исчерпаны.


* Приведу цитату из «Дзен Python» : удобочитаемость имеет значение .
Pythonic = читабельность для меня; i + jпросто визуально легче анализируется, по крайней мере, для моих глаз.

Димитрис Фасаракис Хиллиард
источник
1
Тем не менее, усилия по кодированию для n строк составляют O (n). Тем не менее, это хорошо, пока n мало.
TigerhawkT3
Ваш генератор, вероятно, вызывает больше накладных расходов, чем соединение.
Padraic Cunningham
5
работать "".join([i + j for i, j in zip(l1, l2)])и это, безусловно , самый быстрый
Padraic Cunningham
6
"".join(map("".join, zip(l1, l2)))даже быстрее, хотя и не обязательно более питоническим.
Алекси Торхамо,
63

Быстрая альтернатива

По-другому:

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

Вывод:

'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

скорость

Похоже, это быстрее:

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

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

чем самое быстрое решение:

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

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

Также для больших струн:

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.

Вариация для струн разной длины

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijkl'

Более короткий определяет длину ( zip()эквивалент)

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

Вывод:

AaBbCcDdEeFfGgHhIiJjKkLl

Более длинный определяет длину ( itertools.zip_longest(fillvalue='')эквивалент)

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

Вывод:

AaBbCcDdEeFfGgHhIiJjKkLlMNOPQRSTUVWXYZ
Майк Мюллер
источник
49

С join()и zip().

>>> ''.join(''.join(item) for item in zip(u,l))
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
TigerhawkT3
источник
17
Или''.join(itertools.chain.from_iterable(zip(u, l)))
Blender
1
Это приведет к усечению списка, если один из них короче другого, и zipостановится, когда более короткий список будет полностью повторен.
SuperBiasedMan
5
@SuperBiasedMan - Ага. itertools.zip_longestможно использовать, если это станет проблемой.
TigerhawkT3
18

В Python 2 гораздо более быстрый способ делать что-либо, примерно в 3 раза быстрее, чем нарезка списка для небольших строк и в ~ 30 раз для длинных, - это

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

Однако это не сработает на Python 3. Вы могли бы реализовать что-то вроде

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

но к тому времени вы уже потеряли преимущество над разрезанием списков для небольших строк (это все еще в 20 раз выше скорости для длинных строк), и это еще не работает даже для символов, отличных от ASCII.

FWIW, если будут делать это на массивных струнах и нуждается в каждом цикле, и по какой - то причине должен использовать строки Python ... Вот как это сделать:

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

Поможет и специальный корпус, обычный для более мелких типов. FWIW, это всего в 3 раза больше скорости нарезки списка для длинных строк и в 4–5 раз медленнее для небольших строк.

В любом случае я предпочитаю joinрешения, но, поскольку время было упомянуто в другом месте, я подумал, что могу присоединиться.

Veedrac
источник
16

Если вам нужен самый быстрый способ, вы можете объединить itertools с 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

Но комбинирование izipи chain.from_iterableснова быстрее

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

Также существует существенная разница между chain(*и chain.from_iterable(....

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

Нет такой вещи, как генератор с соединением, передача одного всегда будет медленнее, поскольку python сначала создаст список, используя контент, потому что он выполняет два прохода по данным, один для определения необходимого размера, а другой для фактического выполнения соединение, которое невозможно с помощью генератора:

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

Также, если у вас есть строки разной длины и вы не хотите терять данные, вы можете использовать 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'

Для Python 3 это называется zip_longest

Но для python2 предложение veedrac, безусловно, самое быстрое:

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
Падраик Каннингем
источник
2
почему list?? не нужен
Copperfield
1
не согласно моим тестам, вы теряете время на создание промежуточного списка и это лишает смысла использование итераторов. Timeit "".join(list(...))дает мне 6.715280318699769 и timeit "".join(starmap(...))дает мне 6.46332361384313
Copperfield
1
тогда что, машина зависит ?? потому что независимо от того, где я запускаю тест, я получаю тот же точный результат "".join(list(starmap(add, izip(l1,l2))))медленнее, чем "".join(starmap(add, izip(l1,l2))). Я запускаю тест на своей машине в python 2.7.11 и в python 3.5.1 даже в виртуальной консоли www.python.org с python 3.4.3, и все говорят то же самое, и я запускаю его пару раз и всегда то же самое
Copperfield
Я читал и вижу, что он постоянно строит внутренний список в своей переменной буферов, независимо от того, что вы ему передаете, поэтому больше причин для НЕТ давать ему список
Copperfield
@Copperfield, вы говорите о вызове списка или передаче списка?
Padraic Cunningham
12

Вы также можете сделать это, используя mapи operator.add:

from operator import add

u = 'AAAAA'
l = 'aaaaa'

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

Выход :

'AaAaAaAaAa'

Что делает map, так это то, что она берет каждый элемент из первой итерации uи первые элементы из второй итерации lи применяет функцию, предоставленную в качестве первого аргумента add. Тогда присоединяйся просто присоединяйся к ним.

корень
источник
9

Ответ Джима отличный, но вот мой любимый вариант, если вы не возражаете против парочки импорта:

from functools import reduce
from operator import add

reduce(add, map(add, u, l))
knite
источник
7
Он сказал, что больше всего на питоническом языке, а не на самом хаскеллическом;)
Курт
7

Многие из этих предложений предполагают, что строки имеют одинаковую длину. Возможно, это покрывает все разумные варианты использования, но, по крайней мере, мне кажется, что вы также можете разместить строки разной длины. Или я единственный, кто думает, что сетка должна работать примерно так:

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

Один из способов сделать это:

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:]])
Кристофер Ольссон
источник
5

Мне нравится использовать два fors, имена переменных могут дать подсказку / напоминание о том, что происходит:

"".join(char for pair in zip(u,l) for char in pair)
Нил Фульц
источник
4

Просто чтобы добавить еще один, более простой подход:

st = ""
for char in u:
    st = "{0}{1}{2}".format( st, char, l[ u.index( char ) ] )
WeRelic
источник
4

Чувствует себя немного непитоничным, чтобы не рассматривать здесь ответ с двойным списком, чтобы обрабатывать n строку с усилием O (1):

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

где all_strings- список строк, которые вы хотите перемежать. В вашем случае all_strings = [u, l]. Пример полного использования будет выглядеть так:

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'

Как и многие ответы, самый быстрый? Наверное, нет, но просто и гибко. Кроме того, без особых дополнительных сложностей, это немного быстрее, чем принятый ответ (в общем, добавление строк в 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
источник
Однако все еще не так быстро, как самый быстрый ответ: он получил 50,3 мс на тех же данных и компьютере
scnerd
3

Потенциально быстрее и короче, чем текущее ведущее решение:

from itertools import chain

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

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

Стратегия с точки зрения скорости - делать как можно больше на высшем уровне. То же самое исправление zip_longest () для неровных строк, и оно будет выходить из того же модуля, что и chain (), так что не стоит мне слишком много очков!

Другие решения, которые я придумал по ходу дела:

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

res = "".join(k + l[i] for i, k in enumerate(u))
cdlane
источник
3

Вы можете использовать 1iteration_utilities.roundrobin

u = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
l = 'abcdefghijklmnopqrstuvwxyz'

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

или ManyIterablesкласс из того же пакета:

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

1 Это из библиотеки третьей стороной я написал: iteration_utilities.

MSeifert
источник
2

Я бы использовал zip (), чтобы получить понятный и простой способ:

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

print result
# 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
valeas
источник