转置/解压函数(zip的反函数)是什么?

605

我有一个由2个元素的元组列表,我想将它们转换成2个列表,其中第一个列表包含每个元组中的第一个元素,第二个列表包含第二个元素。

例如:

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

有没有内置函数可以做到这一点?


8
下面有很棒的答案,但也请看一下numpy的转置功能 - opyate
5
请参考这个很好的答案,使用生成器而不是列表来执行相同的操作:如何使用生成器解压缩迭代器 - YvesgereY
为什么zip被称为转置? - Charlie Parker
1
@CharlieParker,因为它类似于数学中的矩阵转置。如果最初每个嵌套序列中的数据被视为矩阵的“行”,那么其值将最终出现在由输出表示的相应矩阵的“列”中。 - Karl Knechtel
1
不是真正的反转,但在某些情况下,d=dict(original) 后跟 d.keys()d.values() 可能很方便。 - djvg
14个回答

908
在2.x中,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(在转换为元组后),否则不必担心参数数量过多。
在3.x中,zip返回一个惰性迭代器,但这很容易转换:
>>> list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)]))
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

27
如果事情能够这么简单就好了。但是以这种方式解压zip([], [])并不能得到[], [],而只是得到了[]。如果事情能够这么简单就好了…… - user2357112
4
这在Python3中不起作用。请参见:https://dev59.com/NoHba4cB1Zd3GeqPO0IT - Tommy
47
@Tommy 这是不正确的。zip 在 Python 3 中的工作方式完全相同,只是返回一个迭代器而不是列表。为了获得与上面相同的输出,您只需要将 zip 调用包装在列表中:list(zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])) 将输出 [('a', 'b', 'c', 'd'), (1, 2, 3, 4)] - MJeffryes
5
长列表可能会导致内存和性能问题。 - Laurent LAPORTE
1
@JohnP:list是可以的。但是如果您尝试一次性实现全部结果(通过将zip的结果转换为list),则可能会使用大量内存(因为必须立即创建所有 tuple)。如果您只能在不进行list转换的情况下迭代zip的结果,则可以节省大量内存。唯一的其他问题是如果输入具有许多元素;成本在于必须将它们全部解包为参数,并且zip将需要为它们创建和存储迭代器。这仅在非常长的list(想象成数十万个或更多元素)中才是真正的问题。 - ShadowRanger
显示剩余9条评论

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

生成器在你要求每个元素之前不会遍历整个列表,但另一方面,它们会保留对原始列表的引用。


11
尤其是如果Python遵循承诺,只有在需要时才会扩展列表推导式。嗯...通常,列表推导式会立即扩展 - 或者我理解错了? - glglgl
1
@glglgl:不,你可能是对的。我只是希望未来的某个版本能开始做正确的事情。(改变并不是不可能的,需要改变的副作用语义可能已经被弃用了。) - Anders Eurenius
11
您希望得到的是一个生成器表达式 - 它已经存在了。 - glglgl
16
这种方法并不比 zip(*x) 更好。zip(*x) 只需要一次循环,而且不会使用堆栈元素。 - habnabit
1
“是否更好地扩展”取决于原始数据的生命周期与转置数据相比。只有在使用案例是立即使用和丢弃转置数据,而原始列表在内存中存储更长时间时,此答案才比使用zip更好。 - Liz Av
显示剩余2条评论

24

我喜欢在我的程序中使用zip(*iterable)(这正是你正在寻找的代码片段),示例:

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

我认为unzip更易读。


21
如果你的列表长度不相同,可能不想使用 zip,就像 Patrick 的回答所示。这个方法可以解决问题:
>>> 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')]

你可以使用不带函数的map来将空结果填充为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
4
对于使用Python3的用户来说,它被称为zip_longest - zezollo
1
@GrijeshChauhan 我知道这已经很老了,但它是一个奇怪的内置功能:https://docs.python.org/2/library/functions.html#map “如果函数为None,则假定为恒等函数;如果有多个参数,则map()返回一个列表,其中包含所有可迭代对象中相应项目的元组(一种转置操作)。可迭代参数可以是序列或任何可迭代对象;结果始终是一个列表。” - cactus1

18

为了获得一个列表的元组,就像问题中所示:

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

我认为这是最准确的答案,因为正如问题所问,它实际上返回了一对列表(而不是元组列表)。 - rusheb

7

天真的方法

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 是自然数集合中的元素,
  • a_ij 对应于第 i 个可迭代对象中的第 j 个元素,

应用 transpose_finite_iterable 后,我们得到

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

Python示例,其中a_ij == jn == 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是一个无限可迭代的有限迭代器(在我们的情况下是元组)。
>>> 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)

这可以被视为在有限非空可迭代对象的二元运算符类中,其自身的反演(数学家称这种函数为"反演函数")。


作为使用 singledispatch 的额外奖励,我们可以像处理其他对象一样处理 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 返回的是迭代器,如果有人想要一个类似于OP中listtuple,可以使用 map内置函数 进一步实现。

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

广告

我已经在 0.5.0 版本的lz中添加了通用解决方案,可以像下面这样使用:

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

附言

对于可能是无限的可迭代对象,以及其中可能包含无限的可迭代对象的情况,目前没有解决方案(至少没有显而易见的)。不过这种情况相对较少出现。


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

我认为更加pythonic的方法是这样解压并返回原始数据:
```python ```
x,y=zip(*XY)

但是这会返回一个元组,如果你需要一个列表,可以使用以下方法:
x,y=(list(x),list(y))

4

考虑使用 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]]     

3

之前的回答都没有有效地提供所需的输出,即列表元组而不是元组列表。对于前者,您可以使用maptuple。这是区别:

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,您需要将结果传递给诸如listtuple的函数以耗尽迭代器。对于内存效率较高的迭代器,您可以省略相应解决方案的外部listtuple调用。

1
这应该是最佳答案。看到其他当前被认为是“顶级”的答案真令人沮丧。 - mkearney

2

虽然numpy数组和pandas可能更好,但当作为unzip(args)调用时,此函数模拟了zip(*args)的行为。

允许生成器(例如Python 3中zip的结果)作为args传递,因为它遍历值。

def unzip(items, cls=list, ocls=tuple):
    """Zip function in reverse.

    :param items: Zipped-like iterable.
    :type  items: iterable

    :param cls: Container factory. Callable that returns iterable containers,
        with a callable append attribute, to store the unzipped items. Defaults
        to ``list``.
    :type  cls: callable, optional

    :param ocls: Outer container factory. Callable that returns iterable
        containers. with a callable append attribute, to store the inner
        containers (see ``cls``). Defaults to ``tuple``.
    :type  ocls: callable, optional

    :returns: Unzipped items in instances returned from ``cls``, in an instance
        returned from ``ocls``.
    """
    # iter() will return the same iterator passed to it whenever possible.
    items = iter(items)

    try:
        i = next(items)
    except StopIteration:
        return ocls()

    unzipped = ocls(cls([v]) for v in i)

    for i in items:
        for c, v in zip(unzipped, i):
            c.append(v)

    return unzipped

使用列表容器,只需运行unzip(zipped),如下所示:

unzip(zip(["a","b","c"],[1,2,3])) == (["a","b","c"],[1,2,3])

要使用双端队列或其他支持 append 的容器,需要传递一个工厂函数。

from collections import deque

unzip([("a",1),("b",2)], deque, list) == [deque(["a","b"]),deque([1,2])]

(装饰cls和/或main_cls以微调容器初始化,如上面最终的断言语句中所示。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接