¿Función de transposición/descompresión (inversa de zip)?
Pregunta
Tengo una lista de tuplas de 2 elementos y me gustaría convertirlas en 2 listas donde la primera contiene el primer elemento de 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])
¿Existe una función incorporada que haga eso?
Solución
zip
¡Es 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 zip
con los argumentos:
zip(('a', 1), ('b', 2), ('c', 3), ('d', 4))
... excepto que los argumentos se pasan a zip
directamente (después de convertirse en una tupla), por lo que no hay necesidad de preocuparse de que la cantidad de argumentos sea demasiado grande.
Otros consejos
También podrías hacer
result = ([ a for a,b in original ], [ b for a,b in original ])
Él debería escalar mejor.Especialmente si Python logra no expandir la lista por comprensión a menos que sea necesario.
(Por cierto, crea 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 lo siguiente:
result = (( a for a,b in original ), ( b for a,b in original ))
Los generadores no analizan la lista hasta que usted pregunta por cada elemento, pero, por otro lado, mantienen referencias a la lista original.
Si tiene listas que no tienen la misma longitud, es posible que no desee utilizar zip según la respuesta de Patrick.Esto funciona:
>>> zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
Pero con listas de diferentes longitudes, 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 un 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)]
Sin embargo, zip() es un poco más rápido.
me gusta usar zip(*iterable)
(que es el fragmento de código que estás buscando) en mis programas de la siguiente manera:
def unzip(iterable):
return zip(*iterable)
Encuentro unzip
más legible.
>>> 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)]
Desempaqueta las dos listas.
Es sólo 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')]
En mi opinión, la forma más pitónica de descomprimirlo y volver al original es esta:
x,y=zip(*XY)
Pero esto devuelve una tupla, por lo que si necesita una lista puede usar:
x,y=(list(x),list(y))
Enfoque ingenuo
def transpose_finite_iterable(iterable):
return zip(*iterable) # `itertools.izip` for Python 2 users
funciona bien para iterables finitos (p. ej.secuencias como list
/tuple
/str
) de iterables (potencialmente infinitos) que se pueden ilustrar como
| |a_00| |a_10| ... |a_n0| |
| |a_01| |a_11| ... |a_n1| |
| |... | |... | ... |... | |
| |a_0i| |a_1i| ... |a_ni| |
| |... | |... | ... |... | |
dónde
n in ℕ
,a_ij
corresponde aj
-ésimo elemento dei
-ésimo iterable,
y después de aplicar transpose_finite_iterable
obtenemos
| |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_iterable
nuevamente para regresar a la estructura del original iterable
porque result
es un iterable infinito de iterables finitos (tuple
s 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 abordar este caso?
...y aquí viene el deque
Después de echar un vistazo a los documentos de itertools.tee
función, existe una receta de Python que con algunas modificaciones 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 una función general para trabajar con iterables de iterables, algunos de los cuales son finitos y otros son potencialmente infinitos usando functools.singledispatch
decorador 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 propia inversa (los matemáticos llaman a este tipo de funciones "involuciones") en clase de operadores binarios sobre iterables finitos no vacíos.
Como bonificación de singledispatch
podemos manejar numpy
matrices como
import numpy as np
...
transpose.register(np.ndarray, np.transpose)
y luego usarlo como
>>> array = np.arange(4).reshape((2,2))
>>> array
array([[0, 1],
[2, 3]])
>>> transpose(array)
array([[0, 2],
[1, 3]])
Nota
Desde transpose
devuelve iteradores y si alguien quiere tener un tuple
de list
Es como en OP: esto se puede hacer adicionalmente con map
funció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
Agregué una solución generalizada a lz
paquete de 0.5.0
versión que se puede utilizar 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 existe una solución (al menos obvia) para manejar iterables potencialmente infinitos de iterables potencialmente infinitos, pero este caso es menos común.
Dado que devuelve tuplas (y puede utilizar toneladas de memoria), el zip(*zipped)
El truco me parece más inteligente que útil.
Aquí hay una función que en realidad 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
original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
#unzip
a1 , a2 = zip(*original)
#make tuple with two list
result=(list(a1),list(a2))
result
resultado=(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
Considere usar more_itertools.descomprimir:
>>> 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]]
Ninguna de las respuestas anteriores eficientemente proporcionar el resultado requerido, que es un tupla de listas, preferible a lista de tuplas.Para el primero, puedes usar tuple
con 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 asumen Python 2.7, donde zip
devuelve una lista en lugar de un iterador.
Para Python 3.x, deberá pasar el resultado a una función como list
o tuple
para agotar el iterador.Para iteradores con uso eficiente de la memoria, puede omitir el exterior list
y tuple
exige las soluciones respectivas.
Mientras zip(*seq)
es muy útil, puede que no sea 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 encuentro 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 quieras hacer con el resultado, la elección de la colección puede marcar una gran diferencia.En mi caso de uso real, usar conjuntos y sin bucle interno es notablemente más rápido que todos los demás enfoques.
Y, como otros han señalado, si hace esto con conjuntos de datos, podría tener sentido usar colecciones Numpy o Pandas.
Así es como puedes transponer una tupla de 2x4 a una tupla de 4x2.
>>> tuple(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))
resultado
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]