Функция транспонирования / распаковки (обратная сторона zip)?

505

У меня есть список кортежей из 2 элементов, и я хотел бы преобразовать их в 2 списка, где первый содержит первый элемент в каждом кортеже, а второй список содержит второй элемент.

Например:

original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
# and I want to become...
result = (['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Есть ли встроенная функция, которая делает это?

Cristian
источник
6
Великолепные ответы ниже, но также посмотрите на транспонирование numpy
opyate
3
Посмотрите этот хороший ответ, чтобы сделать то же самое с генераторами вместо списка: как распаковать итератор
YvesgereY

Ответы:

778

zipэто свой обратный! При условии, что вы используете специальный * оператор.

>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

Это работает путем вызова zipаргументов:

zip(('a', 1), ('b', 2), ('c', 3), ('d', 4))

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

Патрик
источник
20
О, если бы это было так просто. Разархивировать zip([], [])таким способом не получится [], []. Это получает вас []. Если только ...
user2357112 поддерживает Монику
4
Это не работает в Python3. Смотрите: stackoverflow.com/questions/24590614/…
Томми
31
@ Томми Это неправильно. zipработает точно так же в Python 3 за исключением того, что он возвращает итератор вместо списка. Чтобы получить тот же вывод, что и выше, вам просто нужно list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
заключить
4
обратите внимание: вы можете столкнуться с проблемами памяти и производительности с очень длинными списками.
Лоран Лапорт
1
@JohnP: listс хорошо. Но если вы попытаетесь реализовать полный результат все сразу (по listifying результата zip), вы можете использовать много памяти (потому что все , что tupleей необходимо создать сразу). Если вы можете просто перебрать результат zipбез listifying, вы сэкономите много памяти. Единственное другое беспокойство - если вход имеет много элементов; цена его заключается в том, что он должен распаковать их все как аргументы и zipдолжен будет создать и сохранить итераторы для всех них. Это реальная проблема только с очень длинными listсимволами (например, сотнями тысяч элементов или более).
ShadowRanger
29

Вы могли бы также сделать

result = ([ a for a,b in original ], [ b for a,b in original ])

Это должно масштабироваться лучше. Особенно, если Python преуспевает в том, чтобы не расширять список пониманий без необходимости.

(Между прочим, он создает 2-кортеж (пару) списков, а не список кортежей, как это zipделает.)

Если генераторы вместо реальных списков в порядке, это будет сделано так:

result = (( a for a,b in original ), ( b for a,b in original ))

Генераторы не просматривают список, пока вы не запросите каждый элемент, но, с другой стороны, они сохраняют ссылки на исходный список.

Андерс Эурениус
источник
8
«Особенно, если Python преуспевает в том, чтобы не расширять список пониманий без необходимости». ммм ... нормально, списки сразу расширяются - или я что-то не так делаю?
glglgl
1
@glglgl: Нет, вы, вероятно, правы. Я просто надеялся, что какая-то будущая версия может начать делать правильные вещи. (Это не невозможно изменить, семантика побочных эффектов, которая нуждается в изменениях, вероятно, уже не рекомендуется.)
Anders Eurenius
9
То, что вы надеетесь получить, это выражение генератора, которое уже существует.
glglgl
12
Это не «масштабируется лучше», чем zip(*x)версия. zip(*x)требуется только один проход через цикл и не использует элементы стека.
habnabit
1
Будет ли оно «лучше масштабироваться» или нет, зависит от жизненного цикла исходных данных по сравнению с транспонированными данными. Этот ответ только лучше, чем использование, zipесли в сценарии использования транспонированные данные используются и сразу отбрасываются, тогда как исходные списки остаются в памяти гораздо дольше.
Ekevoo
21

Если у вас есть списки, которые не имеют одинаковую длину, вы можете не использовать zip согласно ответу Патрика. Это работает:

>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

Но со списками разной длины zip усекает каждый элемент до длины самого короткого списка:

>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', )])
[('a', 'b', 'c', 'd', 'e')]

Вы можете использовать карту без функции, чтобы заполнить пустые результаты с помощью None:

>>> map(None, *[('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', )])
[('a', 'b', 'c', 'd', 'e'), (1, 2, 3, 4, None)]

Хотя zip () немного быстрее.

Крис
источник
4
Вы также можете использоватьizip_longest
Marcin
3
Известен как zip_longestдля пользователей python3.
Зезолло
1
@GrijeshChauhan Я знаю, что это действительно старая версия, но это странная встроенная функция: docs.python.org/2/library/functions.html#map "Если для функции задано значение None, предполагается, что функция идентичности; если имеется несколько аргументов, map () возвращает список, состоящий из кортежей, содержащих соответствующие элементы из всех итераций (своего рода операция транспонирования). Итерируемые аргументы могут быть последовательностью или любым итерируемым объектом; результатом всегда является список. "
cactus1
18

Мне нравится использовать zip(*iterable)(это фрагмент кода, который вы ищете) в моих программах, так:

def unzip(iterable):
    return zip(*iterable)

Я нахожу unzipболее читабельным.

wassimans
источник
12
>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> tuple([list(tup) for tup in zip(*original)])
(['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Дает кортеж списков, как в вопросе.

list1, list2 = [list(tup) for tup in zip(*original)]

Распаковывает два списка.

Noyer282
источник
8

Наивный подход

def transpose_finite_iterable(iterable):
    return zip(*iterable)  # `itertools.izip` for Python 2 users

отлично работает для конечных итерируемых (например, последовательностей типа list/ tuple/ str) (потенциально бесконечных) итерируемых, которые можно проиллюстрировать как

| |a_00| |a_10| ... |a_n0| |
| |a_01| |a_11| ... |a_n1| |
| |... | |... | ... |... | |
| |a_0i| |a_1i| ... |a_ni| |
| |... | |... | ... |... | |

где

  • n in ℕ,
  • a_ijсоответствует -ому jэлементу i-й итерируемой,

и после подачи заявления transpose_finite_iterableмы получаем

| |a_00| |a_01| ... |a_0i| ... |
| |a_10| |a_11| ... |a_1i| ... |
| |... | |... | ... |... | ... |
| |a_n0| |a_n1| ... |a_ni| ... |

Python пример такого случая , когда a_ij == j,n == 2

>>> from itertools import count
>>> iterable = [count(), count()]
>>> result = transpose_finite_iterable(iterable)
>>> next(result)
(0, 0)
>>> next(result)
(1, 1)

Но мы не можем использовать transpose_finite_iterableснова, чтобы вернуться к структуре оригинала, iterableпотому что resultэто бесконечная итерация конечных итераций ( tupleв нашем случае s):

>>> transpose_finite_iterable(result)
... hangs ...
Traceback (most recent call last):
  File "...", line 1, in ...
  File "...", line 2, in transpose_finite_iterable
MemoryError

Итак, как мы можем справиться с этим делом?

... и вот идет deque

После того, как мы посмотрим на документы по itertools.teeфункциям , есть рецепт Python, который с некоторыми изменениями может помочь в нашем случае

def transpose_finite_iterables(iterable):
    iterator = iter(iterable)
    try:
        first_elements = next(iterator)
    except StopIteration:
        return ()
    queues = [deque([element])
              for element in first_elements]

    def coordinate(queue):
        while True:
            if not queue:
                try:
                    elements = next(iterator)
                except StopIteration:
                    return
                for sub_queue, element in zip(queues, elements):
                    sub_queue.append(element)
            yield queue.popleft()

    return tuple(map(coordinate, queues))

Давайте проверим

>>> from itertools import count
>>> iterable = [count(), count()]
>>> result = transpose_finite_iterables(transpose_finite_iterable(iterable))
>>> result
(<generator object transpose_finite_iterables.<locals>.coordinate at ...>, <generator object transpose_finite_iterables.<locals>.coordinate at ...>)
>>> next(result[0])
0
>>> next(result[0])
1

Синтез

Теперь мы можем определить общую функцию для работы с итерациями итерируемых, одни из которых конечны, а другие потенциально бесконечны, используя functools.singledispatchдекоратор, такой как

from collections import (abc,
                         deque)
from functools import singledispatch


@singledispatch
def transpose(object_):
    """
    Transposes given object.
    """
    raise TypeError('Unsupported object type: {type}.'
                    .format(type=type))


@transpose.register(abc.Iterable)
def transpose_finite_iterables(object_):
    """
    Transposes given iterable of finite iterables.
    """
    iterator = iter(object_)
    try:
        first_elements = next(iterator)
    except StopIteration:
        return ()
    queues = [deque([element])
              for element in first_elements]

    def coordinate(queue):
        while True:
            if not queue:
                try:
                    elements = next(iterator)
                except StopIteration:
                    return
                for sub_queue, element in zip(queues, elements):
                    sub_queue.append(element)
            yield queue.popleft()

    return tuple(map(coordinate, queues))


def transpose_finite_iterable(object_):
    """
    Transposes given finite iterable of iterables.
    """
    yield from zip(*object_)

try:
    transpose.register(abc.Collection, transpose_finite_iterable)
except AttributeError:
    # Python3.5-
    transpose.register(abc.Mapping, transpose_finite_iterable)
    transpose.register(abc.Sequence, transpose_finite_iterable)
    transpose.register(abc.Set, transpose_finite_iterable)

который можно рассматривать как свой собственный обратный (математики называют этот вид функций «инволюциями» ) в классе бинарных операторов над конечными непустыми итерациями.


В качестве бонуса singledispatchмы можем обрабатывать numpyмассивы, такие как

import numpy as np
...
transpose.register(np.ndarray, np.transpose)

а затем использовать его как

>>> array = np.arange(4).reshape((2,2))
>>> array
array([[0, 1],
       [2, 3]])
>>> transpose(array)
array([[0, 2],
       [1, 3]])

Запись

Поскольку transposeвозвращает итераторы , и если кто - то хочет иметь tupleв listс , как в OP - это может быть сделано дополнительно с mapвстроенной функцией , как

>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> tuple(map(list, transpose(original)))
(['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Реклама

Я добавил обобщенное решение для lzпакета из 0.5.0версии, которая может быть использована как

>>> from lz.transposition import transpose
>>> list(map(tuple, transpose(zip(range(10), range(10, 20)))))
[(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), (10, 11, 12, 13, 14, 15, 16, 17, 18, 19)]

PS

Не существует решения (по крайней мере, очевидного) для обработки потенциально бесконечно повторяемых и потенциально бесконечно повторяемых, но этот случай менее распространен.

Азат Ибраков
источник
4

Это всего лишь другой способ сделать это, но он мне очень помог, поэтому я пишу это здесь:

Имея эту структуру данных:

X=[1,2,3,4]
Y=['a','b','c','d']
XY=zip(X,Y)

В результате чего:

In: XY
Out: [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]

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

x,y=zip(*XY)

Но это возвращает кортеж, поэтому, если вам нужен список, вы можете использовать:

x,y=(list(x),list(y))
GM
источник
3

Рассмотрите возможность использования more_itertools.unzip :

>>> from more_itertools import unzip
>>> original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> [list(x) for x in unzip(original)]
[['a', 'b', 'c', 'd'], [1, 2, 3, 4]]     
Нил Г
источник
1

Поскольку он возвращает кортежи (и может использовать тонны памяти), zip(*zipped)уловка кажется мне более умной, чем полезной.

Вот функция, которая на самом деле даст вам обратную сторону zip.

def unzip(zipped):
    """Inverse of built-in zip function.
    Args:
        zipped: a list of tuples

    Returns:
        a tuple of lists

    Example:
        a = [1, 2, 3]
        b = [4, 5, 6]
        zipped = list(zip(a, b))

        assert zipped == [(1, 4), (2, 5), (3, 6)]

        unzipped = unzip(zipped)

        assert unzipped == ([1, 2, 3], [4, 5, 6])

    """

    unzipped = ()
    if len(zipped) == 0:
        return unzipped

    dim = len(zipped[0])

    for i in range(dim):
        unzipped = unzipped + ([tup[i] for tup in zipped], )

    return unzipped
Waylon Flinn
источник
Постоянное воссоздание кортежей не кажется мне эффективным, но вы могли бы расширить этот подход, используя запросы, которые могли бы выделять память.
Чарли Кларк,
0

Ни один из предыдущих ответов эффективно не обеспечивает требуемый вывод, который является кортежем списков , а не списком кортежей . Для первого вы можете использовать tupleс map. Вот разница:

res1 = list(zip(*original))              # [('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
res2 = tuple(map(list, zip(*original)))  # (['a', 'b', 'c', 'd'], [1, 2, 3, 4])

Кроме того, большинство предыдущих решений предполагает Python 2.7, где zipвозвращает список, а не итератор.

Для Python 3.x вам нужно будет передать результат в функцию, например, listили tupleисчерпать итератор. Для итераторов с эффективным использованием памяти вы можете опустить внешние listи tupleвызовы соответствующих решений.

JPP
источник
0

Хотя zip(*seq)это очень полезно, оно может быть неподходящим для очень длинных последовательностей, поскольку оно создаст кортеж значений для передачи. Например, я работал с системой координат с более чем миллионом записей и нашел, что ее создание значительно быстрее последовательности напрямую.

Общий подход будет выглядеть примерно так:

from collections import deque
seq = ((a1, b1, …), (a2, b2, …), …)
width = len(seq[0])
output = [deque(len(seq))] * width # preallocate memory
for element in seq:
    for s, item in zip(output, element):
        s.append(item)

Но, в зависимости от того, что вы хотите сделать с результатом, выбор коллекции может иметь большое значение. В моем реальном случае использования наборы без внутреннего цикла заметно быстрее всех других подходов.

И, как уже отмечали другие, если вы делаете это с наборами данных, возможно, имеет смысл вместо этого использовать коллекции Numpy или Pandas.

Чарли Кларк
источник
0

В то время как числовые массивы и панды могут быть предпочтительнее, эта функция имитирует поведение zip(*args)при вызове как unzip(args).

Позволяет передавать генераторы так, argsкак они перебирают значения. Украсить clsи / или main_clsмикроуправлять контейнером инициализации.

def unzip(items, cls=list, main_cls=tuple):
    """Zip function in reverse.

    :param items: Zipped-like iterable.
    :type  items: iterable

    :param cls: Callable that returns iterable with callable append attribute.
        Defaults to `list`.
    :type  cls: callable, optional

    :param main_cls: Callable that returns iterable with callable append
        attribute. Defaults to `tuple`.
    :type  main_cls: callable, optional

    :returns: Unzipped items in instances returned from `cls`, in an instance
        returned from `main_cls`.

    :Example:

        assert unzip(zip(["a","b","c"],[1,2,3])) == (["a","b",c"],[1,2,3])
        assert unzip([("a",1),("b",2),("c",3)]) == (["a","b","c"],[1,2,3])
        assert unzip([("a",1)], deque, list) == [deque(["a"]),deque([1])]
        assert unzip((["a"],["b"]), lambda i: deque(i,1)) == (deque(["b"]),)
    """
    items = iter(items)

    try:
        i = next(items)
    except StopIteration:
        return main_cls()

    unzipped = main_cls(cls([v]) for v in i)

    for i in items:
        for c,v in zip(unzipped,i):
            c.append(v)

    return unzipped
Trasp
источник