转置/解压缩函数(逆压缩)?

我有一个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])

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

196293 次浏览

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(在被转换为元组之后),所以没有必要担心参数的数量变得太大。

你也可以

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

应该规模更好。特别是如果Python不扩展列表推导式,除非需要的话。

(顺便说一句,它生成一个二元组(对)列表,而不是像zip那样生成一个元组列表。)

如果生成器代替实际的列表是可以的,这将做到:

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

在您请求每个元素之前,生成器不会仔细检查列表,但另一方面,它们会保留对原始列表的引用。

如果您的列表长度不相同,您可能不希望按照patrick的回答使用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')]

你可以使用map没有函数来填充空结果为None:

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

不过Zip()稍微快一点。

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

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

我发现unzip更具可读性。

这只是另一种方法,但它对我帮助很大,所以我写在这里:

具有这种数据结构的:

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

在我看来,更python化的解压方法是这样的:

x,y=zip(*XY)

但是这个返回一个元组,所以如果你需要一个列表,你可以使用:

x,y=(list(x),list(y))
>>> 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)]

解包两个列表。

因为它返回元组(并且可能使用大量内存),所以对我来说,zip(*zipped)技巧似乎更聪明而不是有用。

这是一个函数,它会给出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

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

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调用。

虽然zip(*seq)非常有用,但它可能不适合非常长的序列,因为它将创建一个要传入的值元组。例如,我一直在使用一个拥有超过100万个条目的坐标系,并发现直接创建序列要快得多。

一般的方法是这样的:

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集合可能是有意义的。

天真的方法

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

在哪里

  • # EYZ0,
  • a_ij对应于i-th迭代对象的j-th元素,

在应用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是有限可迭代对象的无限可迭代对象(在我们的例子中是tuples):

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

那么我们如何处理这种情况呢?

... 这是deque

在我们看了# EYZ0函数的文档之后,有一个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

合成

现在我们可以用# EYZ0装饰定义一个通用函数来处理可迭代对象的可迭代对象其中一个是有限的,另一个可能是无限的

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返回迭代器,如果有人想要一个tuplelists,就像在OP中一样——这可以额外使用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版本添加了# EYZ0包的通用解决方案,可以像这样使用

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

注:

没有解决方案(至少是明显的)来处理潜在无限迭代对象的潜在无限迭代对象,但这种情况不太常见。

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

虽然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), as

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

要使用deques或其他任何带有append的容器,请传递一个工厂函数。

from collections import deque


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

(装饰cls和/或main_cls来微管理容器初始化,如上面最后的断言语句所简单显示的那样。)

下面是一个简单的单行回答,可以产生所需的输出:

original = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
list(zip(*original))
# [('a', 'b', 'c', 'd'), (1, 2, 3, 4)]