Funzione Transpose / Unzip (inverse of zip)?


505

Ho un elenco di tuple a 2 elementi e vorrei convertirle in 2 elenchi in cui il primo contiene il primo elemento in ciascuna tupla e il secondo elenco contiene il secondo elemento.

Per esempio:

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

Esiste una funzione integrata che lo fa?


6
Grandi risposte di seguito, ma guarda anche la trasposizione di
numpy

3
Vedi questa bella risposta per fare lo stesso con i generatori invece dell'elenco: come decomprimere un iteratore
YvesgereY,

Risposte:


778

zipè il suo contrario! A condizione di utilizzare l'operatore speciale *.

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

Il modo in cui funziona è chiamando zipcon gli argomenti:

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

... tranne per il fatto che gli argomenti vengono passati zipdirettamente (dopo essere stati convertiti in una tupla), quindi non è necessario preoccuparsi che il numero di argomenti diventi troppo grande.


20
Oh, se solo fosse così semplice. Decomprimere in zip([], [])questo modo non ti ottiene [], []. Ti prende []. Se solo ...
user2357112 supporta Monica il

4
Questo non funziona in Python3. Vedi: stackoverflow.com/questions/24590614/…
Tommy,

31
@ Tommy Questo non è corretto. zipfunziona esattamente lo stesso in Python 3 tranne per il fatto che restituisce un iteratore anziché un elenco. Per ottenere lo stesso output di cui sopra devi solo racchiudere la chiamata zip in un elenco: list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))verrà emesso[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
MJeffryes

4
avviso: è possibile incontrare problemi di memoria e prestazioni con elenchi molto lunghi.
Laurent LAPORTE,

1
@JohnP: lists vanno bene. Ma se provi a realizzare il risultato completo tutto in una volta ( listcercando il risultato di zip), potresti usare molta memoria (perché tutti gli tuples devono essere creati contemporaneamente). Se riesci a scorrere il risultato zipsenza listifying, risparmierai molta memoria. L'unica altra preoccupazione è se l'input ha molti elementi; il costo è che deve decomprimerli tutti come argomenti e zipdovrà creare e archiviare iteratori per tutti. Questo è solo un vero problema con s molto lunghi list(pensa a centinaia di migliaia di elementi o più).
ShadowRanger,

29

Potresti anche fare

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

Si dovrebbe scalare meglio. Soprattutto se Python riesce a non espandere la comprensione dell'elenco a meno che non sia necessario.

(Per inciso, crea una 2 tupla (coppia) di elenchi, piuttosto che un elenco di tuple, come zipfa.

Se i generatori invece degli elenchi effettivi sono ok, ciò farebbe quanto segue:

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

I generatori non sgranocchiano l'elenco finché non chiedi ogni elemento, ma d'altra parte mantengono i riferimenti all'elenco originale.


8
"Soprattutto se Python riesce a non espandere la comprensione dell'elenco se non è necessario." mmm ... normalmente, la comprensione dell'elenco si espande immediatamente - o sbaglio qualcosa?
glglgl,

1
@glglgl: No, probabilmente hai ragione. Speravo solo che qualche versione futura potesse iniziare a fare la cosa giusta. (Non è impossibile cambiare, la semantica degli effetti collaterali che ha bisogno di cambiamenti è probabilmente già scoraggiata.)
Anders Eurenius,

9
Ciò che speri di ottenere è un'espressione del generatore, che esiste già.
glglgl,

12
Questo non "ridimensiona" rispetto alla zip(*x)versione. zip(*x)richiede solo un passaggio attraverso il ciclo e non utilizza gli elementi dello stack.
habnabit,

1
Se "ridimensiona meglio" o meno dipende dal ciclo di vita dei dati originali rispetto ai dati trasposti. Questa risposta è migliore dell'uso solo zipse il caso d'uso è che i dati trasposti vengono utilizzati e scartati immediatamente, mentre gli elenchi originali rimangono in memoria per molto più tempo.
Ekevoo,

21

Se hai elenchi che non hanno la stessa lunghezza, potresti non voler usare zip come per la risposta di Patricks. Questo funziona:

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

Ma con elenchi di lunghezze diverse, zip tronca ogni elemento alla lunghezza dell'elenco più breve:

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

Puoi usare la mappa senza alcuna funzione per riempire i risultati vuoti con Nessuno:

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

zip () è comunque leggermente più veloce.


4
Potresti anche usareizip_longest
Marcin il

3
Conosciuto come zip_longestper gli utenti di python3.
Zezollo,

1
@GrijeshChauhan So che questo è davvero vecchio, ma è una strana funzione integrata : docs.python.org/2/library/functions.html#map "Se la funzione è Nessuna, si presume la funzione identità; se ci sono più argomenti, map () restituisce un elenco composto da tuple contenenti gli elementi corrispondenti di tutti gli iterabili (una sorta di operazione di trasposizione). Gli argomenti iterabili possono essere una sequenza o qualsiasi oggetto iterabile; il risultato è sempre un elenco. "
cactus1,

18

Mi piace usare zip(*iterable)(che è il pezzo di codice che stai cercando) nei miei programmi così:

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

Trovo unzippiù leggibile.


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

Fornisce una tupla di elenchi come nella domanda.

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

Disimballa i due elenchi.


8

Approccio ingenuo

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

funziona bene per iterabili finiti (es. sequenze come list/ tuple/ str) di iterabili (potenzialmente infiniti) che possono essere illustrati come

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

dove

  • n in ℕ,
  • a_ijcorrisponde a j-th element of i-th iterable,

e dopo l'applicazione transpose_finite_iterableotteniamo

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

Esempio Python di tale caso in cui 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)

Ma non possiamo usarlo di transpose_finite_iterablenuovo per tornare alla struttura dell'originale iterableperché resultè un iterabile infinito di iterabili finiti ( tuples nel nostro caso):

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

Quindi, come possiamo affrontare questo caso?

... e qui arriva il deque

Dopo aver dato un'occhiata ai documenti di itertools.teefunzione , c'è la ricetta di Python che con alcune modifiche può aiutare nel nostro caso

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

controlliamo

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

Sintesi

Ora possiamo definire una funzione generale per lavorare con iterabili di iterabili di cui sono finiti e altri potenzialmente potenzialmente infiniti usando functools.singledispatchdecorator come

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)

che può essere considerato come proprio inverso (i matematici chiamano questo tipo di funzioni "involuzioni" ) in classe di operatori binari su iterabili non vuoti finiti.


Come bonus di singledispatching possiamo gestire numpyarray come

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

e poi usalo come

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

Nota

Dal momento che transposerestituisce iteratori e se qualcuno vuole avere una tupledi lists come in OP - questo può essere fatto in aggiunta con mapla funzione incorporata come

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

Annuncio pubblicitario

Ho aggiunto una soluzione generalizzata al lzpacchetto dalla 0.5.0versione che può essere utilizzata come

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

Non esiste una soluzione (almeno ovvia) per la gestione di iterabili potenzialmente infiniti di iterabili potenzialmente infiniti, ma questo caso è però meno comune.


4

È solo un altro modo di farlo, ma mi ha aiutato molto, quindi lo scrivo qui:

Avere questa struttura di dati:

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

Con il risultato di:

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

Il modo più pitone di decomprimerlo e tornare all'originale è questo secondo me:

x,y=zip(*XY)

Ma questo restituisce una tupla, quindi se hai bisogno di un elenco puoi usare:

x,y=(list(x),list(y))

3

Prendi in considerazione l'utilizzo di 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

Dal momento che restituisce tuple (e può usare tonnellate di memoria), il zip(*zipped)trucco mi sembra più intelligente che utile.

Ecco una funzione che ti darà effettivamente il contrario di 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

Ricreare continuamente le tuple non mi sembra così efficiente, ma potresti estendere questo approccio usando deques che potrebbero preallocare la memoria.
Charlie Clark,

0

Nessuna delle risposte precedenti fornisce in modo efficiente l'output richiesto, che è una tupla di elenchi , piuttosto che un elenco di tuple . Per il primo, puoi usare tuplecon map. Ecco la differenza:

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

Inoltre, la maggior parte delle soluzioni precedenti presuppone Python 2.7, in cui ziprestituisce un elenco anziché un iteratore.

Per Python 3.x, dovrai passare il risultato a una funzione come listo tupleper esaurire l'iteratore. Per gli iteratori efficienti in termini di memoria, è possibile omettere l'esterno liste tuplerichiedere le rispettive soluzioni.


0

Sebbene zip(*seq)sia molto utile, potrebbe non essere adatto a sequenze molto lunghe in quanto creerà una tupla di valori da trasmettere. Ad esempio, ho lavorato con un sistema di coordinate con oltre un milione di voci e lo trovo significativamente più veloce da creare le sequenze direttamente.

Un approccio generico sarebbe qualcosa del genere:

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)

Ma, a seconda di cosa vuoi fare con il risultato, la scelta della collezione può fare una grande differenza. Nel mio caso d'uso reale, usando set e nessun loop interno, è notevolmente più veloce di tutti gli altri approcci.

E, come altri hanno notato, se lo fai con set di dati, potrebbe avere senso usare invece le raccolte Numpy o Pandas.


0

Mentre matrici e panda intorpiditi possono essere preferibili, questa funzione imita il comportamento di zip(*args)quando chiamato come unzip(args).

Consente il passaggio dei generatori argsmentre scorre i valori. Decorare clse / o main_clsmicro gestire l'inizializzazione del contenitore.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.