Перемістити / розпакувати функцію (зворотній zip)?


505

У мене є список двоярусних кортежів, і я хотів би перетворити їх у 2 списки, де перший містить перший елемент у кожному кортежі, а другий - другий.

Наприклад:

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

Чи є вбудована функція, яка це робить?


6
Чудові відповіді нижче, але також дивіться транспорту нумпі
опияте

3
Дивіться цю приємну відповідь, щоб зробити те ж саме з генераторами замість списку: how-to-unzip-an-iterator
YvesgereY

Відповіді:


778

zipце своя зворотна! За умови використання спеціального * оператора.

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

Як це працює, викликаючи zipаргументи:

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

… Крім аргументів передаються zipбезпосередньо (після перетворення в кортеж), тому не потрібно турбуватися про те, що кількість аргументів стає занадто великою.


20
О, якби це було так просто. Розпакувавши zip([], [])цей спосіб, ви не отримаєте [], []. Це отримує тебе []. Якби тільки ...
user2357112 підтримує Моніку

4
Це не працює в Python3. Дивіться: stackoverflow.com/questions/24590614/…
Томмі

31
@Tommy Це неправильно. zipпрацює точно так само в Python 3, за винятком того, що він повертає ітератор замість списку. Для того, щоб отримати той самий вихід, що і вище, вам просто потрібно загорнути поштовий дзвінок у список: list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))виведе[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]
MJeffryes

4
зауважте: ви можете зустріти проблеми з пам’яттю та продуктивністю з дуже довгими списками.
Лоран ЛАПОРТЕ

1
@JohnP: lists - це добре. Але якщо ви спробуєте реалізувати повний результат все відразу (по listifying результату zip), ви можете використовувати багато пам'яті (тому що все , що tupleїй необхідно створити відразу). Якщо ви зможете просто повторити результат, zipне listпотураючи, ви заощадите багато пам’яті. Єдине інше питання - якщо вхід має багато елементів; вартість там полягає в тому, що він повинен розпакувати їх як аргументи, і для zipних потрібно буде створити та зберігати ітератори. Це лише реальна проблема з дуже довгими lists (подумайте, сотні тисяч елементів і більше).
ShadowRanger

29

Ви також можете зробити

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

Він повинен краще масштабуватися. Особливо, якщо Python добре допомагає не розширювати розуміння списку, якщо це не потрібно.

(Між іншим, він складає 2-кратні (пари) списків, а не список кортежів, як zipце робиться.)

Якщо генератори замість фактичних списків не в порядку, це буде так:

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

Генератори не перебирають список, поки ви не попросите кожного елемента, але, з іншого боку, вони зберігають посилання на вихідний список.


8
"Особливо, якщо Python добре допомагає не розширювати розуміння списку, якщо це не потрібно". ммм ... як правило, розуміння списку розгортаються негайно - чи я щось помиляюсь?
glglgl

1
@glglgl: Ні, ти, мабуть, маєш рацію. Я просто сподівався, що якась майбутня версія може почати робити правильно. (Змінити це не неможливо. Семантика побічних ефектів, яка потребує змін, ймовірно, вже відмовляє.)
Андер Евреній

9
Що ви сподіваєтесь отримати - це генераторний вираз - який вже існує.
glglgl

12
Це не "масштабніше", ніж zip(*x)версія. zip(*x)потрібен лише один прохід через цикл і не використовує елементи стека.
ханабіт

1
Незалежно від того, чи «вона масштабніша», чи ні, залежить від життєвого циклу вихідних даних порівняно з транспонованими даними. Ця відповідь є кращою, ніж використання, zipякщо випадком використання є те, що переміщені дані використовуються та відкидаються негайно, тоді як оригінальні списки залишаються в пам'яті набагато довше.
Екеву

21

Якщо у вас є списки не однакової довжини, ви, можливо, не захочете використовувати zip відповідно до відповіді Патріка. Це працює:

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

Але зі списками різної довжини, zip скорочує кожен елемент до довжини найкоротшого списку:

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

Ви можете використовувати карту без функції, щоб заповнити порожні результати значенням None:

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

zip () незначно швидше.


4
Ви також можете скористатисяizip_longest
Marcin

3
Відомий як zip_longestдля користувачів python3.
zezollo

1
@GrijeshChauhan Я знаю, що це дійсно старе, але це дивна вбудована функція: docs.python.org/2/library/functions.html#map "Якщо функція" Ніяка ", функція ідентичності передбачається; якщо є кілька аргументів, map () повертає список, що складається з кортежів, що містять відповідні елементи з усіх ітерабелів (вид операції транспонування). Ітерабельні аргументи можуть бути послідовністю або будь-яким ітерабельним об'єктом; результат завжди є списком. "
кактус1

18

Мені подобається використовувати zip(*iterable)(який фрагмент коду ви шукаєте) у своїх програмах так:

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

Мені здається, unzipчитабельніше.


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

Надає список списків, як у запитанні.

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

Розпаковується два списки.


8

Наївний підхід

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

прекрасно працює для кінцево ітерабельних (наприклад, послідовностей, як list/ tuple/ str) (потенційно нескінченних) ітерабелів, які можна проілюструвати як

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

де

  • n in ℕ,
  • a_ijвідповідає j-му елементу i-й ітерабельний,

і після застосування transpose_finite_iterableми отримуємо

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

Приклад Python такого випадку, коли 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)

Але ми не можемо використати transpose_finite_iterableзнову, щоб повернутися до структури оригіналу, iterableоскільки resultце нескінченний ітерабел кінцевих ітерабелів ( tupleу нашому випадку):

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

То як ми можемо розібратися з цією справою?

... і ось ось deque

Після того, як ми розглянемо документи itertools.teeфункції , є рецепт Python, який з деякою модифікацією може допомогти в нашому випадку

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

давайте перевіримо

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

Синтез

Тепер ми можемо визначити загальну функцію для роботи з ітерабелями ітерабелів, один з яких є кінцевим, а інший - потенційно нескінченним, використовуючи functools.singledispatchдекоратор, як

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)

що може розглядатися як власна зворотна (математики називають цей вид функцій "інволюціями" ) в класі бінарних операторів над кінцевими не порожніми ітерабелями.


Як бонус singledispatching, ми можемо обробляти numpyмасиви на зразок

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

а потім використовувати його як

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

Примітка

Оскільки transposeповертає ітератори , і якщо хто - то хоче мати tupleв listс , як в OP - це може бути зроблено додатково з mapвбудованою функцією , як

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

Реклама

Я додав узагальнене рішення до lzпакета з 0.5.0версії, яку можна використовувати як

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

Не існує рішення (принаймні очевидного) для поводження з потенційно нескінченним ітерабелом потенційно нескінченних ітерабелів, але цей випадок зустрічається рідше.


4

Це лише інший спосіб зробити це, але він мені дуже допоміг, тому я пишу це тут:

Маючи цю структуру даних:

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

Результат:

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

На мій погляд, більш пітонічний спосіб розкрутити його та повернутися до оригіналу:

x,y=zip(*XY)

Але це повернення кортежу, тому якщо вам потрібен список, ви можете використовувати:

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

3

Подумайте про використання 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

Оскільки він повертає кортежі (і може використовувати тонни пам’яті), zip(*zipped)фокус здається мені більш розумним, ніж корисним.

Ось функція, яка насправді надасть вам зворотній код блискавки.

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

Постійне відтворення кортежів не здається мені таким ефективним, але ви можете розширити цей підхід, використовуючи деки, які можуть попередньо виділити пам'ять.
Чарлі Кларк

0

Жоден з попередніх відповідей ефективно не забезпечують необхідну потужність, який є кортежем списків , а не список кортежів . Для перших ви можете використовувати tupleс map. Ось різниця:

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

Крім того, більшість попередніх рішень передбачає Python 2.7, де zipповертає список, а не ітератор.

Для Python 3.x вам потрібно буде передати результат такій функції, як listабо tupleвичерпати ітератор. Для ітераторів, що працюють на пам'яті, ви можете опустити зовнішнє listі tupleвикликати відповідні рішення.


0

Хоча zip(*seq)це дуже корисно, воно може бути непридатним для дуже довгих послідовностей, оскільки це створить набір значень, які потрібно передавати. Наприклад, я працював із системою координат з понад мільйон записів і знаходжу це значно швидше для створення послідовності безпосередньо.

Загальний підхід буде приблизно таким:

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)

Але, в залежності від того, що ви хочете зробити з результатом, вибір колекції може мати велике значення. У моєму фактичному випадку використання, використовуючи набори та відсутність внутрішнього циклу, помітно швидше, ніж усі інші підходи.

І, як зазначали інші, якщо ви робите це з наборами даних, можливо, замість цього використовуватись колекції Numpy або Pandas.


0

Хоча nummy масиви та панди можуть бути кращими, ця функція імітує поведінку, zip(*args)коли викликається як unzip(args).

Дозволяє передавати генераторам так, argsяк він повторюється через значення. Прикрасити clsта / або main_clsмікроконтролювати ініціалізацію контейнера.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.