Función de transposición / descompresión (inversa de zip)?

505

Tengo una lista de tuplas de 2 elementos y me gustaría convertirlas en 2 listas donde la primera contiene el primer elemento en cada tupla y la segunda lista contiene el segundo elemento.

Por ejemplo:

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

¿Hay una función integrada que hace eso?

Cristian
fuente
66
Grandes respuestas a continuación, pero también mira la transposición de
numpy
3
Vea esta buena respuesta para hacer lo mismo con los generadores en lugar de la lista: cómo descomprimir un iterador
YvesgereY

Respuestas:

778

zipes su propio inverso! Siempre que utilice el operador especial *.

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

La forma en que esto funciona es llamando zipcon los argumentos:

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

... excepto que los argumentos se pasan zipdirectamente (después de convertirse en una tupla), por lo que no hay necesidad de preocuparse por el número de argumentos que se hacen demasiado grandes.

Patricio
fuente
20
Oh, si tan solo fuera tan simple. Descomprimir de zip([], [])esta manera no te atrapa [], []. Le consigue []. Si solo ...
user2357112 es compatible con Monica
44
Esto no funciona en Python3. Ver: stackoverflow.com/questions/24590614/…
Tommy
31
@Tommy Esto es incorrecto. zipfunciona exactamente igual en Python 3, excepto que devuelve un iterador en lugar de una lista. Para obtener el mismo resultado que el anterior, solo necesita incluir la llamada zip en una lista: list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))generará[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
MJeffryes
44
aviso: puede resolver problemas de memoria y rendimiento con listas muy largas.
Laurent LAPORTE
1
@JohnP: lists están bien. Pero si intenta obtener el resultado completo de una sola vez (si listaplica el resultado de zip), podría usar mucha memoria (porque todos los tuples deben crearse a la vez). Si solo puede iterar sobre el resultado zipsin listifying, ahorrará mucha memoria. La única otra preocupación es si la entrada tiene muchos elementos; el costo es que debe desempaquetarlos a todos como argumentos y zipnecesitará crear y almacenar iteradores para todos ellos. Esto es solo un problema real con s muy largos list(piense en cientos de miles de elementos o más).
ShadowRanger
29

También podrías hacer

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

se debe escalar mejor. Especialmente si Python hace bien en no expandir las comprensiones de la lista a menos que sea necesario.

(Por cierto, hace una lista de 2 tuplas (par), en lugar de una lista de tuplas, como zip hace).

Si los generadores en lugar de las listas reales están bien, esto haría eso:

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

Los generadores no se mueven a través de la lista hasta que solicite cada elemento, pero por otro lado, mantienen referencias a la lista original.

Anders Eurenius
fuente
8
"Especialmente si Python hace bien en no expandir las comprensiones de la lista a menos que sea necesario". mmm ... normalmente, las comprensiones de listas se expanden de inmediato, ¿o me sale algo mal?
glglgl
1
@glglgl: No, probablemente tengas razón. Solo esperaba que alguna versión futura pudiera comenzar a hacer lo correcto. (No es imposible cambiar, la semántica de efectos secundarios que necesita cambios probablemente ya esté desaconsejada.)
Anders Eurenius
99
Lo que espera obtener es una expresión generadora, que ya existe.
glglgl
12
Esto no 'escala mejor' que la zip(*x)versión. zip(*x)solo requiere una pasada a través del bucle y no utiliza elementos de la pila.
habnabit
1
Si "escala mejor" o no depende del ciclo de vida de los datos originales en comparación con los datos transpuestos. Esta respuesta es mejor que usarla zipsi el caso de uso es que los datos transpuestos se usan y descartan de inmediato, mientras que las listas originales permanecen en la memoria por mucho más tiempo.
Ekevoo
21

Si tiene listas que no tienen la misma longitud, es posible que no desee usar zip según la respuesta de Patricks. Esto funciona:

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

Pero con diferentes listas de longitud, zip trunca cada elemento a la longitud de la lista más corta:

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

Puede usar el mapa sin función para llenar resultados vacíos con Ninguno:

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

zip () es marginalmente más rápido sin embargo.

Chris
fuente
44
También podría usarizip_longest
Marcin
3
Conocido como zip_longestpara los usuarios de python3.
zezollo
1
@GrijeshChauhan Sé que esto es muy antiguo, pero es una característica extraña incorporada: docs.python.org/2/library/functions.html#map "Si la función es Ninguna, se asume la función de identidad; si hay varios argumentos, map () devuelve una lista que consta de tuplas que contienen los elementos correspondientes de todos los iterables (un tipo de operación de transposición). Los argumentos iterables pueden ser una secuencia o cualquier objeto iterable; el resultado es siempre una lista ".
cactus1
18

Me gusta usar zip(*iterable)(que es el código que estás buscando) en mis programas así:

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

Me parece unzipmás legible.

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

Da una tupla de listas como en la pregunta.

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

Descomprime las dos listas.

Noyer282
fuente
8

Enfoque ingenuo

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

funciona bien para iterables finitos (por ejemplo, secuencias como list/ tuple/ str) de iterables (potencialmente infinitos) que pueden ilustrarse como

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

dónde

  • n in ℕ,
  • a_ijcorresponde al jelemento i-th de -th iterable,

y después de aplicar transpose_finite_iterableobtenemos

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

Ejemplo de Python de tal caso donde 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)

Pero no podemos usar transpose_finite_iterablenuevamente para volver a la estructura del original iterableporque resultes un iterable infinito de iterables finitos ( tuples en nuestro caso):

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

Entonces, ¿cómo podemos lidiar con este caso?

... y aquí viene el deque

Después de echar un vistazo a los documentos de itertools.teefunción , hay una receta de Python que con alguna modificación puede ayudar en nuestro 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))

vamos a revisar

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

Síntesis

Ahora podemos definir la función general para trabajar con iterables de iterables, de los cuales son finitos y otros potencialmente infinitos usando functools.singledispatchdecorador como

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)

que puede considerarse como su propio inverso (los matemáticos llaman a este tipo de funciones "involuciones" ) en la clase de operadores binarios sobre iterables finitos no vacíos.


Como beneficio adicional singledispatch, podemos manejar numpymatrices como

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

y luego úsalo como

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

Nota

Desde transposedevuelve iteradores y si alguien quiere tener una tuplede lists como en la OP - esto se puede hacer, además, con la mapfunción incorporada como

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

Anuncio

He agregado una solución generalizada al lzpaquete desde la 0.5.0versión que se puede usar como

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

PD

No hay una solución (al menos obvia) para manejar iterables potencialmente infinitos de iterables potencialmente infinitos, pero este caso es menos común.

Azat Ibrakov
fuente
4

Es solo otra forma de hacerlo, pero me ayudó mucho, así que lo escribo aquí:

Teniendo esta estructura de datos:

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

Resultando en:

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

La forma más pitónica de descomprimirlo y volver al original es esta en mi opinión:

x,y=zip(*XY)

Pero esto devuelve una tupla, por lo que si necesita una lista puede usar:

x,y=(list(x),list(y))
GM
fuente
3

Considere usar 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
fuente
1

Como devuelve tuplas (y puede usar toneladas de memoria), el zip(*zipped) truco me parece más inteligente que útil.

Aquí hay una función que realmente le dará la inversa 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
fuente
La recreación continua de tuplas no me parece tan eficiente, pero podría ampliar este enfoque utilizando deques que podrían preasignar memoria.
Charlie Clark el
0

Ninguna de las respuestas anteriores proporciona eficientemente el resultado requerido, que es una tupla de listas , en lugar de una lista de tuplas . Para el primero, puedes usar tuplecon map. Aquí está la diferencia:

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

Además, la mayoría de las soluciones anteriores suponen Python 2.7, donde zipdevuelve una lista en lugar de un iterador.

Para Python 3.x, deberá pasar el resultado a una función como listo tupleagotar el iterador. Para los iteradores con uso eficiente de la memoria, puede omitir el exterior listy tuplesolicitar las soluciones respectivas.

jpp
fuente
0

Mientras zip(*seq) es muy útil, puede no ser adecuado para secuencias muy largas, ya que creará una tupla de valores para pasar. Por ejemplo, he estado trabajando con un sistema de coordenadas con más de un millón de entradas y creo que es mucho más rápido crear Las secuencias directamente.

Un enfoque genérico sería algo como esto:

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)

Pero, dependiendo de lo que quiera hacer con el resultado, la elección de la colección puede marcar una gran diferencia. En mi caso de uso real, el uso de conjuntos y sin bucle interno es notablemente más rápido que todos los demás enfoques.

Y, como otros han señalado, si está haciendo esto con conjuntos de datos, podría tener sentido usar colecciones de Numpy o Pandas.

Charlie Clark
fuente
0

Si bien las matrices y pandas numpy pueden ser preferibles, esta función imita el comportamiento de zip(*args)cuando se llama como unzip(args).

Permite que los generadores se pasen a argsmedida que itera por los valores. Decorar clsy / o main_clsmicro gestionar la inicialización del contenedor.

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