Fonction Transposer / Décompresser (inverse du zip)?

505

J'ai une liste de tuples à 2 éléments et je voudrais les convertir en 2 listes où le premier contient le premier élément dans chaque tuple et la deuxième liste contient le deuxième élément.

Par exemple:

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

Y a-t-il une fonction intégrée qui fait cela?

Cristian
la source
6
Excellentes réponses ci-dessous, mais regardez également la transposition de
numpy
3
Voir cette belle réponse pour faire la même chose avec les générateurs au lieu de la liste: comment dézipper un itérateur
YvesgereY

Réponses:

778

zipest son propre inverse! À condition d'utiliser l'opérateur spécial *.

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

La façon dont cela fonctionne est en appelant zipavec les arguments:

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

… Sauf que les arguments sont passés zipdirectement (après avoir été convertis en tuple), il n'y a donc pas lieu de s'inquiéter du nombre d'arguments trop volumineux.

Patrick
la source
20
Oh, si seulement c'était si simple. Décompresser de zip([], [])cette façon ne vous permet pas [], []. Ça vous prend []. Si seulement ...
user2357112 prend en charge Monica le
4
Cela ne fonctionne pas en Python3. Voir: stackoverflow.com/questions/24590614/…
Tommy
31
@Tommy C'est incorrect. zipfonctionne exactement de la même manière dans Python 3, sauf qu'il renvoie un itérateur au lieu d'une liste. Afin d'obtenir la même sortie que ci-dessus, vous avez juste besoin d'envelopper l'appel zip dans une liste: list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))affichera[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
MJeffryes
4
remarque: vous pouvez rencontrer des problèmes de mémoire et de performances avec de très longues listes.
Laurent LAPORTE
1
@JohnP: tout va listbien. Mais si vous essayez de réaliser le résultat complet en une seule fois (en listimitant le résultat de zip), vous pouvez utiliser beaucoup de mémoire (car tous les tuples doivent être créés en même temps). Si vous pouvez simplement répéter le résultat de zipsans listifying, vous économiserez beaucoup de mémoire. La seule autre préoccupation est de savoir si l'entrée comporte de nombreux éléments; le coût est qu'il doit les déballer tous comme arguments, et zipdevra créer et stocker des itérateurs pour chacun d'eux. Ce n'est qu'un vrai problème avec deslist s très longs (pensez à des centaines de milliers d'éléments ou plus).
ShadowRanger
29

Vous pourriez aussi faire

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

Il devrait mieux évoluer. Surtout si Python fait bien de ne pas étendre les compréhensions de liste à moins que cela ne soit nécessaire.

(Incidemment, cela fait une liste de 2 tuple (paire), plutôt qu'une liste de tuples, comme le zipfait.)

Si les générateurs au lieu des listes réelles sont corrects, cela ferait cela:

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

Les générateurs ne parcourent pas la liste jusqu'à ce que vous demandiez chaque élément, mais d'un autre côté, ils gardent les références à la liste d'origine.

Anders Eurenius
la source
8
"Surtout si Python réussit à ne pas étendre les compréhensions de liste, sauf si cela est nécessaire." mmm ... normalement, les compréhensions de liste sont étendues immédiatement - ou est-ce que je me trompe?
glglgl
1
@glglgl: Non, vous avez probablement raison. J'espérais juste qu'une future version pourrait commencer à faire la bonne chose. (Il n'est pas impossible de changer, la sémantique des effets secondaires qui nécessitent des changements est probablement déjà découragée.)
Anders Eurenius
9
Ce que vous espérez obtenir, c'est une expression de générateur - qui existe déjà.
glglgl
12
Cela ne «dimensionne pas mieux» que la zip(*x)version. zip(*x)ne nécessite qu'un seul passage dans la boucle et n'utilise pas les éléments de pile.
habnabit
1
Qu'elle "évolue mieux" ou non dépend du cycle de vie des données d'origine par rapport aux données transposées. Cette réponse est meilleure que l'utilisation zipsi le cas d'utilisation est que les données transposées sont utilisées et rejetées immédiatement, tandis que les listes originales restent en mémoire beaucoup plus longtemps.
Ekevoo
21

Si vous avez des listes qui ne sont pas de la même longueur, vous ne voudrez peut-être pas utiliser zip comme indiqué dans la réponse de Patrick. Cela marche:

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

Mais avec des listes de longueurs différentes, zip tronque chaque élément à la longueur de la liste la plus courte:

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

Vous pouvez utiliser la carte sans fonction pour remplir les résultats vides avec Aucun:

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

zip () est cependant légèrement plus rapide.

Chris
la source
4
Vous pouvez également utiliserizip_longest
Marcin
3
Connu comme zip_longestpour les utilisateurs de python3.
zezollo
1
@GrijeshChauhan Je sais que c'est vraiment vieux, mais c'est une fonctionnalité intégrée bizarre: docs.python.org/2/library/functions.html#map "Si la fonction est None, la fonction d'identité est supposée; s'il y a plusieurs arguments, map () renvoie une liste composée de tuples contenant les éléments correspondants de tous les itérables (une sorte d'opération de transposition). Les arguments itérables peuvent être une séquence ou tout autre objet itérable; le résultat est toujours une liste. "
cactus1
18

J'aime utiliser zip(*iterable)(qui est le morceau de code que vous recherchez) dans mes programmes comme suit:

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

Je trouve unzipplus lisible.

wassimans
la source
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])

Donne un tuple de listes comme dans la question.

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

Décompresse les deux listes.

Noyer282
la source
8

Approche naïve

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

fonctionne très bien pour des itérables finis (par exemple des séquences comme list/ tuple/ str) d'itérables (potentiellement infinis) qui peuvent être illustrés comme

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

  • n in ℕ,
  • a_ijcorrespond au j-ème élément du i-ème itérable,

et après avoir appliqué, transpose_finite_iterablenous obtenons

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

Exemple Python d'un tel cas où 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)

Mais nous ne pouvons pas utiliser à transpose_finite_iterablenouveau pour revenir à la structure de l'original iterablecar il results'agit d'un itérable infini d'itérables finis ( tuples dans notre cas):

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

Alors, comment pouvons-nous traiter ce cas?

... et voici deque

Après avoir regardé les documents de itertools.teefonction , il existe une recette Python qui, avec quelques modifications, peut aider dans notre cas

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

Allons vérifier

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

Synthèse

Maintenant, nous pouvons définir une fonction générale pour travailler avec des itérables dont certains sont finis et d'autres potentiellement infinis en utilisant un functools.singledispatchdécorateur comme

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)

qui peut être considérée comme son propre inverse (les mathématiciens appellent ce type de fonctions "involutions" ) dans la classe des opérateurs binaires sur des itérables finis non vides.


En prime, singledispatchnous pouvons gérer des numpytableaux comme

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

puis l'utiliser comme

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

Remarque

Depuis transposeretourne les itérateurs et si quelqu'un veut avoir un tuplede lists comme dans OP - cela peut être fait en plus avec une mapfonction intégrée comme

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

Publicité

J'ai ajouté une solution généralisée au lzpackage à partir de la 0.5.0version qui peut être utilisée comme

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

Il n'y a pas de solution (du moins évidente) pour gérer les itérables potentiellement infinis des itérables potentiellement infinis, mais ce cas est cependant moins courant.

Azat Ibrakov
la source
4

Ce n'est qu'une autre façon de le faire, mais cela m'a beaucoup aidé, alors je l'écris ici:

Ayant cette structure de données:

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

Résultant en:

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

La façon la plus pythonique de le décompresser et de revenir à l'original est celle-ci à mon avis:

x,y=zip(*XY)

Mais cela retourne un tuple donc si vous avez besoin d'une liste, vous pouvez utiliser:

x,y=(list(x),list(y))
GM
la source
3

Pensez à utiliser 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
la source
1

Puisqu'il renvoie des tuples (et peut utiliser des tonnes de mémoire), l' zip(*zipped)astuce me semble plus intelligente qu'utile.

Voici une fonction qui vous donnera en fait l'inverse de 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
la source
La recréation continue de tuples ne me semble pas très efficace, mais vous pouvez étendre cette approche en utilisant des deques qui pourraient préallouer la mémoire.
Charlie Clark
0

Aucune des réponses précédentes ne fournit efficacement la sortie requise, qui est un tuple de listes , plutôt qu'une liste de tuples . Pour les premiers, vous pouvez utiliser tupleavec map. Voici la différence:

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

De plus, la plupart des solutions précédentes supposent Python 2.7, où ziprenvoie une liste plutôt qu'un itérateur.

Pour Python 3.x, vous devrez passer le résultat à une fonction telle que listou tuplepour épuiser l'itérateur. Pour les itérateurs économes en mémoire, vous pouvez omettre l'extérieur listet tupleappeler les solutions respectives.

jpp
la source
0

Bien qu'il zip(*seq)soit très utile, il peut ne pas convenir à de très longues séquences car il créera un tuple de valeurs à transmettre. Par exemple, j'ai travaillé avec un système de coordonnées avec plus d'un million d'entrées et je le trouve beaucoup plus rapide à créer directement les séquences.

Une approche générique serait quelque chose comme ceci:

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)

Mais, selon ce que vous voulez faire du résultat, le choix de la collection peut faire une grande différence. Dans mon cas d'utilisation réel, l'utilisation d'ensembles et sans boucle interne est sensiblement plus rapide que toutes les autres approches.

Et, comme d'autres l'ont noté, si vous faites cela avec des ensembles de données, il peut être judicieux d'utiliser à la place des collections Numpy ou Pandas.

Charlie Clark
la source
0

Alors que les tableaux et les pandas engourdis peuvent être préférables, cette fonction imite le comportement de zip(*args)lorsqu'il est appelé as unzip(args).

Permet aux générateurs d'être transmis au argsfur et à mesure de l'itération des valeurs. Décorez clset / ou main_clsmicrogérez l'initialisation du conteneur.

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