Funkcja transpozycji / rozpakowania (odwrotna do zip)?

505

Mam listę krotek z 2 elementami i chciałbym przekonwertować je na 2 listy, w których pierwsza zawiera pierwszą pozycję w każdej krotce, a druga lista zawiera drugą pozycję.

Na przykład:

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

Czy istnieje wbudowana funkcja, która to robi?

Cristian
źródło
6
Świetne odpowiedzi poniżej, ale także spójrz na transpozycję
numpy
3
Zobacz tę przyjemną odpowiedź, aby zrobić to samo z generatorami zamiast z listą: jak rozpakować-iterator
YvesgereY

Odpowiedzi:

778

zipjest własną odwrotnością! Pod warunkiem, że korzystasz ze specjalnego * operatora.

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

Działa to poprzez wywołanie zipargumentów:

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

… Z wyjątkiem argumentów przekazywanych zipbezpośrednio (po przekonwertowaniu na krotkę), więc nie trzeba się martwić, że liczba argumentów stanie się zbyt duża.

Patrick
źródło
20
Och, gdyby to było takie proste. Rozpakowanie w zip([], [])ten sposób cię nie dostanie [], []. Dostaje cię []. Jeśli tylko ...
użytkownik2357112 obsługuje Monikę
4
To nie działa w Python3. Zobacz: stackoverflow.com/questions/24590614/…
Tommy
31
@Tommy To jest nieprawidłowe. zipdziała dokładnie tak samo w Pythonie 3, z tym wyjątkiem, że zwraca iterator zamiast listy. Aby uzyskać taki sam wynik, jak powyżej, wystarczy zawinąć wywołanie zip w listę: list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))wyświetli[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
MJeffryes
4
Uwaga: możesz napotkać problemy z pamięcią i wydajnością za pomocą bardzo długich list.
Laurent LAPORTE
1
@JohnP: lists są w porządku. Ale jeśli spróbujesz realizować pełny rezultat naraz (przez listifying wynik zip), można użyć dużo pamięci (ponieważ wszystkie te tuples muszą być tworzone na raz). Jeśli możesz po prostu powtórzyć wynik zipbez listifying, zaoszczędzisz dużo pamięci. Jedynym innym problemem jest to, czy dane wejściowe zawierają wiele elementów; istnieje koszt, że musi rozpakować je wszystkie jako argumenty i zipbędzie musiał utworzyć i przechowywać iteratory dla wszystkich. To tylko prawdziwy problem z bardzo długimi lists (pomyśl o setkach tysięcy elementów lub więcej).
ShadowRanger,
29

Ty też możesz to zrobić

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

Powinno być lepiej skalowane. Zwłaszcza jeśli Python robi dobrze, jeśli nie jest to konieczne, aby nie rozszerzać listy.

(Nawiasem mówiąc, tworzy 2-krotkę (parę) list, a nie listę krotek, jak to ziprobi.)

Jeśli generatory zamiast rzeczywistych list są w porządku, to by to zrobiło:

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

Generatory nie przeszukują listy, dopóki nie poprosisz o każdy element, ale z drugiej strony zachowują odniesienia do oryginalnej listy.

Anders Eurenius
źródło
8
„Zwłaszcza, jeśli Python robi dobrze, jeśli nie jest to konieczne, nie rozszerzając listy.” mmm ... normalnie, lista wyrażeń jest natychmiast rozszerzana - czy coś jest nie tak?
glglgl,
1
@glglgl: Nie, prawdopodobnie masz rację. Miałem tylko nadzieję, że przyszła wersja zacznie działać właściwie. (Nie jest to niemożliwe do zmiany, semantyka skutków ubocznych, która wymaga zmian, prawdopodobnie już jest odradzana.)
Anders Eurenius
9
To, co masz nadzieję uzyskać, to ekspresja generatora - która już istnieje.
glglgl
12
To nie „skaluje się lepiej” niż zip(*x)wersja. zip(*x)wymaga tylko jednego przejścia przez pętlę i nie zużywa elementów stosu.
habnabit
1
To, czy „skaluje się lepiej”, czy nie, zależy od cyklu życia oryginalnych danych w porównaniu do transponowanych danych. Ta odpowiedź jest lepsza niż użycie, zipjeśli przypadek użycia polega na tym, że transponowane dane są natychmiast wykorzystywane i odrzucane, podczas gdy oryginalne listy pozostają w pamięci znacznie dłużej.
Ekevoo,
21

Jeśli masz listy, które nie są tej samej długości, możesz nie chcieć używać zip zgodnie z odpowiedzią Patricks. To działa:

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

Ale przy różnych listach długości zip obcina każdy element do długości najkrótszej listy:

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

Możesz użyć mapy bez funkcji do wypełnienia pustych wyników brakiem:

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

zip () jest jednak nieznacznie szybszy.

Chris
źródło
4
Możesz również użyćizip_longest
Marcin
3
Znany jako zip_longestdla użytkowników python3.
zezollo
1
@GrijeshChauhan Wiem, że to jest naprawdę stara, ale to dziwna wbudowana funkcja: docs.python.org/2/library/functions.html#map "Jeśli funkcja jest Brak, funkcja tożsamości jest zakładana; jeśli istnieje wiele argumentów, map () zwraca listę składającą się z krotek zawierających odpowiednie elementy ze wszystkich iteracji (rodzaj operacji transpozycji). Argumentami iterowalnymi może być sekwencja lub dowolny obiekt iteracyjny; wynikiem jest zawsze lista. ”
cactus1
18

Lubię używać zip(*iterable)(który jest fragmentem kodu, którego szukasz) w moich programach, ponieważ:

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

Uważam, że jest unzipbardziej czytelny.

wassimans
źródło
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])

Daje krotkę list jak w pytaniu.

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

Rozpakowuje dwie listy.

Noyer282
źródło
8

Naiwne podejście

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

działa dobrze w przypadku iteracji skończonej (np. sekwencji takich jak list/ tuple/ str) (potencjalnie nieskończonych) iteracji, które można zilustrować jak

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

gdzie

  • n in ℕ,
  • a_ijodpowiada j-temu elementowi i-tej iterowalnej,

a po złożeniu wniosku transpose_finite_iterableotrzymujemy

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

Pyton przykład takiego przypadku 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)

Ale nie możemy użyć transpose_finite_iterableponownie, aby powrócić do struktury oryginału, iterableponieważ resultjest nieskończoną iteracją iteracji skończonych ( tuplew naszym przypadku):

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

Jak więc poradzić sobie z tą sprawą?

... i oto nadchodzi deque

Po przyjrzeniu się dokumentom itertools.teefunkcji istnieje przepis w języku Python, który z pewnymi modyfikacjami może pomóc w naszym przypadku

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

Sprawdźmy

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

Synteza

Teraz możemy zdefiniować ogólną funkcję do pracy z iterowalnymi elementami iteracyjnymi, z których niektóre są skończone, a inne są potencjalnie nieskończone za pomocą functools.singledispatchdekoratora takiego jak

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)

które można uznać za własną odwrotność (matematycy nazywają tego rodzaju funkcje „inwolucjami” ) w klasie operatorów binarnych w stosunku do skończonych niepustych iteratów.


Jako bonus singledispatching możemy obsługiwać numpytablice takie jak

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

a następnie użyj go jak

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

Uwaga

Ponieważ transposezwroty iteratory i jeśli ktoś chce mieć tuplez lists jak w PO - to może być wykonane dodatkowo z mapwbudowaną funkcję jak

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

Reklama

Dodałem uogólnione rozwiązanie do lzpakietu z 0.5.0wersji, z której można korzystać jak

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

Nie ma rozwiązania (przynajmniej oczywistego) dla obsługi potencjalnie nieskończonej iteracji potencjalnie nieskończonych iteracji, ale ten przypadek jest jednak mniej powszechny.

Azat Ibrakov
źródło
4

To tylko inny sposób, aby to zrobić, ale bardzo mi pomogło, więc piszę tutaj:

Posiadanie tej struktury danych:

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

Wynikające z:

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

Moim zdaniem bardziej pythonowy sposób na rozpakowanie go i powrót do oryginału:

x,y=zip(*XY)

Ale to zwraca krotkę, więc jeśli potrzebujesz listy, możesz użyć:

x,y=(list(x),list(y))
GM
źródło
3

Rozważ użycie 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]]     
Neil G.
źródło
1

Ponieważ zwraca krotki (i może zużywać mnóstwo pamięci), zip(*zipped)sztuczka wydaje mi się bardziej sprytna niż przydatna.

Oto funkcja, która da ci odwrotność 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
źródło
Ciągłe odtwarzanie krotek nie wydaje mi się tak wydajne, ale możesz rozszerzyć to podejście za pomocą deques, które mogłyby wstępnie przydzielić pamięć.
Charlie Clark,
0

Żadna z poprzednich odpowiedzi nie zapewnia wydajnego wyniku, który jest krotką list , a nie listą krotek . W przypadku tych pierwszych możesz używać tuplez map. Oto różnica:

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

Ponadto większość poprzednich rozwiązań zakłada Python 2.7, w którym zipzwraca listę zamiast iteratora.

W przypadku Python 3.x musisz przekazać wynik do funkcji takiej jak listlub tupledo wyczerpania iteratora. W przypadku iteratorów energooszczędnych można pominąć zewnętrzne listi tuplewzywa do odpowiednich rozwiązań.

jpp
źródło
0

Chociaż zip(*seq)jest to bardzo przydatne, może być nieodpowiednie dla bardzo długich sekwencji, ponieważ stworzy krotkę wartości, które zostaną przekazane. Na przykład pracowałem z układem współrzędnych z ponad milionem wpisów i stwierdziłem, że tworzenie go jest znacznie szybsze sekwencje bezpośrednio.

Ogólne podejście wyglądałoby mniej więcej tak:

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)

Ale w zależności od tego, co chcesz zrobić z wynikiem, wybór kolekcji może mieć duże znaczenie. W moim przypadku użycie zestawów i brak wewnętrznej pętli jest zauważalnie szybsze niż wszystkie inne podejścia.

I, jak zauważyli inni, jeśli robisz to z zestawami danych, warto zamiast tego używać kolekcji Numpy lub Pandas.

Charlie Clark
źródło
0

Chociaż tablice i pandy numpy mogą być preferowane, ta funkcja imituje zachowanie zip(*args)wywołane jako unzip(args).

Umożliwia przekazywanie generatorów w argsmiarę iteracji wartości. Udekoruj clsi / lub main_clsmikro zarządzaj inicjalizacją kontenera.

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
źródło