Transponier- / Entpackungsfunktion (invers von zip)?

505

Ich habe eine Liste mit Tupeln mit zwei Elementen und möchte sie in zwei Listen konvertieren, wobei das erste das erste Element in jedem Tupel enthält und die zweite Liste das zweite Element enthält.

Zum Beispiel:

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

Gibt es eine eingebaute Funktion, die das macht?

Cristian
quelle
6
Tolle Antworten unten, aber sehen Sie sich auch Numpys Transponierung an
opyate
3
Sehen Sie diese nette Antwort, um dasselbe mit Generatoren anstelle der Liste zu tun: wie man einen Iterator
entpackt

Antworten:

778

zipist seine eigene Umkehrung! Vorausgesetzt, Sie verwenden den speziellen * -Operator.

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

Dies funktioniert durch Aufrufen zipmit den folgenden Argumenten:

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

… Außer dass die Argumente zipdirekt an übergeben werden (nachdem sie in ein Tupel konvertiert wurden), sodass Sie sich keine Sorgen machen müssen, dass die Anzahl der Argumente zu groß wird.

Patrick
quelle
20
Oh, wenn es nur so einfach wäre. Das Entpacken auf zip([], [])diese Weise bringt Sie nicht [], []. Es bringt dich []. Wenn nur ...
user2357112 unterstützt Monica
4
Dies funktioniert in Python3 nicht. Siehe: stackoverflow.com/questions/24590614/…
Tommy
31
@ Tommy Das ist falsch. zipfunktioniert in Python 3 genauso, außer dass ein Iterator anstelle einer Liste zurückgegeben wird. Um die gleiche Ausgabe wie oben zu erhalten, müssen Sie nur den Zip-Aufruf in eine Liste list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
einschließen
4
Hinweis: Sie können Speicher- und Leistungsprobleme mit sehr langen Listen lösen.
Laurent LAPORTE
1
@ JohnP: lists sind in Ordnung. Aber wenn Sie versuchen , das vollständige Ergebnis zu realisieren alle auf einmal (durch listdas Ergebnis der ifying zip), könnten Sie viel Speicher verwenden (weil alle die tuples muss auf einmal erstellt werden). Wenn Sie das Ergebnis zipnur listdurchlaufen können, ohne zu zögern, sparen Sie viel Speicher. Das einzige andere Problem ist, ob die Eingabe viele Elemente enthält. Die Kosten dort sind, dass sie alle als Argumente entpacken zipmüssen und Iteratoren für alle von ihnen erstellen und speichern müssen. Dies ist nur bei sehr langen lists ein echtes Problem (denken Sie an Hunderttausende von Elementen oder mehr).
ShadowRanger
29

Sie könnten auch tun

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

Es sollte besser skalieren. Vor allem, wenn Python es gut macht, das Listenverständnis nicht zu erweitern, es sei denn, dies wird benötigt.

(Im Übrigen wird ein 2-Tupel (Paar) von Listen erstellt, anstatt wie hier eine Liste von Tupeln zip.)

Wenn Generatoren anstelle von tatsächlichen Listen in Ordnung sind, würde dies Folgendes bewirken:

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

Die Generatoren durchsuchen die Liste erst, wenn Sie nach jedem Element fragen. Andererseits behalten sie Verweise auf die ursprüngliche Liste bei.

Anders Eurenius
quelle
8
"Vor allem, wenn Python es gut macht, das Listenverständnis nicht zu erweitern, wenn es nicht benötigt wird." mmm ... normalerweise wird das Listenverständnis sofort erweitert - oder verstehe ich etwas falsch?
glglgl
1
@glglgl: Nein, du hast wahrscheinlich recht. Ich hatte nur gehofft, dass eine zukünftige Version das Richtige tun könnte. (Es ist nicht unmöglich zu ändern, die Nebenwirkungssemantik, die geändert werden muss, wird wahrscheinlich bereits entmutigt.)
Anders Eurenius
9
Was Sie hoffen zu bekommen, ist ein Generatorausdruck - der bereits existiert.
glglgl
12
Dies ist nicht besser skalierbar als die zip(*x)Version. zip(*x)erfordert nur einen Durchgang durch die Schleife und verbraucht keine Stapelelemente.
Hablabit
1
Ob es "besser skaliert" oder nicht, hängt vom Lebenszyklus der Originaldaten im Vergleich zu den transponierten Daten ab. Diese Antwort ist nur dann besser als die Verwendung, zipwenn der Anwendungsfall darin besteht, dass die transponierten Daten sofort verwendet und verworfen werden, während die ursprünglichen Listen viel länger im Speicher bleiben.
Ekevoo
21

Wenn Sie Listen haben, die nicht die gleiche Länge haben, möchten Sie möglicherweise nicht die Zip-Datei gemäß der Antwort von Patricks verwenden. Das funktioniert:

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

Bei Listen mit unterschiedlicher Länge schneidet zip jedes Element auf die Länge der kürzesten Liste ab:

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

Sie können eine Karte ohne Funktion verwenden, um leere Ergebnisse mit Keine zu füllen:

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

zip () ist allerdings etwas schneller.

Chris
quelle
4
Sie könnten auch verwendenizip_longest
Marcin
3
Bekannt als zip_longestfür Python3-Benutzer.
Zezollo
1
@GrijeshChauhan Ich weiß, dass dies wirklich alt ist, aber es ist eine seltsame eingebaute Funktion: docs.python.org/2/library/functions.html#map "Wenn function None ist, wird die Identitätsfunktion angenommen; wenn es mehrere Argumente gibt, map () gibt eine Liste zurück, die aus Tupeln besteht, die die entsprechenden Elemente aus allen iterablen Elementen enthalten (eine Art Transponierungsoperation). Die iterierbaren Argumente können eine Sequenz oder ein beliebiges iterierbares Objekt sein; das Ergebnis ist immer eine Liste. "
Kaktus1
18

Ich verwende zip(*iterable)(das ist der Code, den Sie suchen) gerne in meinen Programmen wie folgt:

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

Ich finde unzipmehr lesbar.

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

Gibt ein Tupel von Listen wie in der Frage.

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

Entpackt die beiden Listen.

Noyer282
quelle
8

Naiver Ansatz

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

funktioniert gut für endliche iterierbare (z. B. Sequenzen wie list/ tuple/ str) von (möglicherweise unendlichen) iterablen, die wie folgt dargestellt werden können

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

wo

  • n in ℕ,
  • a_ijentspricht dem j-ten Element des i-ten iterierbaren,

und nach der Bewerbung transpose_finite_iterablebekommen wir

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

Python-Beispiel für einen solchen Fall a_ij == j, in demn == 2

>>> from itertools import count
>>> iterable = [count(), count()]
>>> result = transpose_finite_iterable(iterable)
>>> next(result)
(0, 0)
>>> next(result)
(1, 1)

Aber wir können nicht transpose_finite_iterablewieder verwenden, um zur Struktur des Originals zurückzukehren, iterableda resultes sich um eine unendliche Iteration endlicher Iterablen handelt ( tuplein unserem Fall s):

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

Wie können wir mit diesem Fall umgehen?

... und hier kommt die deque

Nachdem wir uns die itertools.teeFunktionsdokumente angesehen haben , gibt es ein Python-Rezept, das mit einigen Änderungen in unserem Fall helfen kann

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

Lass uns nachsehen

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

Synthese

Jetzt können wir allgemeine Funktionen für die Arbeit mit Iterablen von Iterablen definieren, von denen einige endlich und andere potenziell unendlich sind, wenn functools.singledispatchDekorateure wie verwendet werden

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)

Dies kann als seine eigene Umkehrung (Mathematiker nennen diese Art von Funktionen "Involutionen" ) in der Klasse von Binäroperatoren über endliche nicht leere Iterablen betrachtet werden.


Als Bonus singledispatchkönnen wir mit numpyArrays wie umgehen

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

und dann benutze es gerne

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

Hinweis

Da transposegibt Iteratoren zurück und wenn jemand ein tuplevon lists wie in OP haben möchte - kann dies zusätzlich mit mapeingebauter Funktion wie gemacht werden

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

Werbung

Ich habe verallgemeinerte Lösung hinzugefügt lzPaket aus 0.5.0Version , die verwendet werden kann , wie

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

Es gibt keine (zumindest offensichtliche) Lösung für die Behandlung von potenziell unendlich iterierbaren oder potenziell unendlich iterierbaren Elementen, aber dieser Fall ist weniger häufig.

Azat Ibrakov
quelle
4

Es ist nur ein anderer Weg, aber es hat mir sehr geholfen, also schreibe ich es hier:

Mit dieser Datenstruktur:

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

Ergebend:

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

Der pythonischere Weg, es zu entpacken und zum Original zurückzukehren, ist meiner Meinung nach dieser:

x,y=zip(*XY)

Dies gibt jedoch ein Tupel zurück. Wenn Sie also eine Liste benötigen, können Sie Folgendes verwenden:

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

Erwägen Sie die Verwendung von 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.
quelle
1

Da es Tupel zurückgibt (und Tonnen von Speicher verbrauchen kann), zip(*zipped)scheint mir der Trick eher klug als nützlich zu sein.

Hier ist eine Funktion, die Ihnen tatsächlich die Umkehrung von zip gibt.

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
quelle
Das kontinuierliche Neuerstellen von Tupeln scheint mir nicht so effizient zu sein, aber Sie können diesen Ansatz mithilfe von Deques erweitern, die Speicher vorab zuweisen können.
Charlie Clark
0

Keine der vorherigen Antworten liefert effizient die erforderliche Ausgabe, bei der es sich eher um ein Tupel von Listen als um eine Liste von Tupeln handelt . Für die erstere können Sie tuplemit verwenden map. Hier ist der Unterschied:

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

Darüber hinaus setzen die meisten vorherigen Lösungen Python 2.7 voraus, bei dem zipeine Liste anstelle eines Iterators zurückgegeben wird.

Für Python 3.x müssen Sie das Ergebnis an eine Funktion wie listoder übergeben tuple, um den Iterator zu erschöpfen. Bei speichereffizienten Iteratoren können Sie das Äußere weglassen listund tupledie entsprechenden Lösungen anfordern.

jpp
quelle
0

Dies zip(*seq)ist zwar sehr nützlich, kann jedoch für sehr lange Sequenzen ungeeignet sein, da dadurch ein Tupel von Werten erstellt wird, die übergeben werden sollen. Ich habe beispielsweise mit einem Koordinatensystem mit über einer Million Einträgen gearbeitet und finde es wesentlich schneller zu erstellen die Sequenzen direkt.

Ein generischer Ansatz wäre ungefähr so:

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)

Abhängig davon, was Sie mit dem Ergebnis machen möchten, kann die Wahl der Kollektion einen großen Unterschied machen. In meinem tatsächlichen Anwendungsfall ist die Verwendung von Sets und ohne interne Schleife spürbar schneller als bei allen anderen Ansätzen.

Und wie andere angemerkt haben, kann es sinnvoll sein, stattdessen Numpy- oder Pandas-Sammlungen zu verwenden, wenn Sie dies mit Datasets tun.

Charlie Clark
quelle
0

Während numpy Arrays und Pandas vorzuziehen sind, ahmt diese Funktion das Verhalten nach, zip(*args)wenn sie als aufgerufen wird unzip(args).

Ermöglicht die Übergabe von Generatoren beim argsDurchlaufen von Werten. Dekorieren clsund / oder main_clszur Mikroverwaltung der Containerinitialisierung.

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
quelle