Python:使用索引展平嵌套列表

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python:使用索引展平嵌套列表相关的知识,希望对你有一定的参考价值。

给定一个任意大小的任意深度嵌套列表的列表,我想在树中的所有元素上使用一个平坦的,深度优先的迭代器,但是路径指示也是如此:

for x, y in flatten(L), x == L[y[0]][y[1]]...[y[-1]]. 

那是

L = [[[1, 2, 3], [4, 5]], [6], [7,[8,9]], 10]
flatten(L)

应该产量:

(1, (0, 0, 0)),
(2, (0, 0, 1)),
(3, (0, 0, 2)),
(4, (0, 1, 0)),
(5, (0, 1, 1)),
(6, (1, 0)),
(7, (2, 0)),
(8, (2, 1, 0)),
(9, (2, 1, 1)),
(10, (3,))

我使用带有yield语句的生成器为此做了一个递归实现:

def flatten(l):
    for i, e in enumerate(l):
        try:
            for x, y in flatten(e):
                yield x, (i,) + y
        except:
            yield e, (i,)

但是我不认为它是一个好的或负责任的实现,有没有任何一个方法可以更普遍地使用内置或像itertools这样的std lib?

答案

我认为你自己的解决方案没问题,没有什么比这更简单,而且Python的标准库也无济于事。但是这是另一种方式,它以迭代方式而不是递归方式工作,因此它可以处理非常深层嵌套的列表。

def flatten(l):
    stack = [enumerate(l)]
    path = [None]
    while stack:
        for path[-1], x in stack[-1]:
            if isinstance(x, list):
                stack.append(enumerate(x))
                path.append(None)
            else:
                yield x, tuple(path)
            break
        else:
            stack.pop()
            path.pop()

我将当前“活动”列表保存在一堆enumerate迭代器上,并将当前索引路径保留为另一个堆栈。然后在while循环中,我总是尝试从当前列表中获取下一个元素并适当地处理它:

  • 如果下一个元素是一个列表,那么我将其enumerate迭代器推送到堆栈上,并为索引路径堆栈上的更深层索引腾出空间。
  • 如果下一个元素是一个数字,那么我将它与它的路径一起产生。
  • 如果当前列表中没有下一个元素,那么我从堆栈中删除它(或者更确切地说是它的迭代器)和它的索引点。

演示:

>>> L = [[[1, 2, 3], [4, 5]], [6], [7,[8,9]], 10]
>>> for entry in flatten(L):
        print(entry)

(1, (0, 0, 0))
(2, (0, 0, 1))
(3, (0, 0, 2))
(4, (0, 1, 0))
(5, (0, 1, 1))
(6, (1, 0))
(7, (2, 0))
(8, (2, 1, 0))
(9, (2, 1, 1))
(10, (3,))

请注意,如果您即时处理条目,就像打印一样,那么您可以将路径作为列表生成,即使用yield x, path。演示:

>>> for entry in flatten(L):
        print(entry)

(1, [0, 0, 0])
(2, [0, 0, 1])
(3, [0, 0, 2])
(4, [0, 1, 0])
(5, [0, 1, 1])
(6, [1, 0])
(7, [2, 0])
(8, [2, 1, 0])
(9, [2, 1, 1])
(10, [3])

这样,迭代器只需要整个迭代的O(n)时间,其中n是结构中对象的总数(列表和数字)。当然,打印增加了复杂性,就像创建元组一样。但那就是在发电机之外和打印的“故障”或者你在每条路径上做的任何事情。例如,如果您只查看每个路径的长度而不是其内容,这需要O(1),那么整个事实上甚至实际上是O(n)。

所有这一切,我认为你自己的解决方案是好的。而且显然比这简单。就像我在@ naomik的answer下评论一样,我认为你的解决方案无法处理大约1000或更多的深度列表是相当无关紧要的。人们首先应该没有这样的清单。如果有人这样做,那就是应该修复的错误。如果列表也可以扩展,就像你的情况一样,并且是平衡的,那么即使分支因子只有2,你的内存也会在100以下的深度耗尽,你将无法接近1000。列表不能广泛,那么嵌套列表是错误的数据结构选择,加上你不会对索引路径感兴趣。如果它可以扩展但不宽,那么我会说应该改进创建算法(例如,如果它代表一个排序树,添加平衡)。

再次关于我的解决方案:除了能够处理任意深度列表及其效率之外,我发现其中一些细节值得注意:

  • 你很少看到enumerate对象被存储在某个地方。通常他们只是直接在loop&Co中使用,比如for i, x in enumerate(l):
  • 准备好path[-1]现场并用for path[-1], x in ...写入。
  • 使用带有直接forbreak分支的else循环,迭代下一个单值并且句柄优雅地结束,没有try / except且没有next和一些默认值。
  • 如果你执行yield x, path,即不将每个路径转换为元组,那么你真的需要在迭代期间直接处理它。例如,如果你做list(flatten(L)),那么你得到[(1, []), (2, []), (3, []), (4, []), (5, []), (6, []), (7, []), (8, []), (9, []), (10, [])]。也就是说,“所有”索引路径将为空。当然那是因为实际上只有一个路径对象我会一次又一次地更新和屈服,最后它是空的。这与itertools.groupby非常相似,例如[list(g) for _, g in list(groupby('aaabbbb'))]给你[[], ['b']]。这不是一件坏事。我最近wrote about that extensively

较短的版本,一个堆栈交替地保存索引和enumerate对象:

def flatten(l):
    stack = [None, enumerate(l)]
    while stack:
        for stack[-2], x in stack[-1]:
            if isinstance(x, list):
                stack += None, enumerate(x)
            else:
                yield x, stack[::2]
            break
        else:
            del stack[-2:]
另一答案

从具有默认值的直接递归和状态变量开始,

def flatten (l, i = 0, path = (), acc = []):
  if not l:
    return acc
  else:
    first, *rest = l
    if isinstance (first, list):
      return flatten (first, 0, path + (i,), acc) + flatten (rest, i + 1, path, [])
    else:
      return flatten (rest, i + 1, path, acc + [ (first, path + (i,)) ])

print (flatten (L))
# [ (1, (0, 0, 0))
# , (2, (0, 0, 1))
# , (3, (0, 0, 2))
# , (4, (0, 1, 0))
# , (5, (0, 1, 1))
# , (6, (1, 0))
# , (7, (2, 0))
# , (8, (2, 1, 0))
# , (9, (2, 1, 1))
# , (10, (3,))
# ]

上述程序与您的程序存在相同的弱点;对于深层清单来说,这是不安全的。我们可以使用延续传递样式使其尾递归 - 以粗体更改

def identity (x):
  return x

# tail-recursive, but still not stack-safe, yet
def flatten (l, i = 0, path = (), acc = [], cont = identity):
  if not l:
    return cont (acc)
  else:
    first, *rest = l
    if isinstance (first, list):
      return flatten (first, 0, path + (i,), acc, lambda left:
        flatten (rest, i + 1, path, [], lambda right:
          cont (left + right)))
    else:
      return flatten (rest, i + 1, path, acc + [ (first, path + (i,)) ], cont)


print (flatten (L))
# [ (1, (0, 0, 0))
# , (2, (0, 0, 1))
# , (3, (0, 0, 2))
# , (4, (0, 1, 0))
# , (5, (0, 1, 1))
# , (6, (1, 0))
# , (7, (2, 0))
# , (8, (2, 1, 0))
# , (9, (2, 1, 1))
# , (10, (3,))
# ]

最后,我们用我们自己的call机制替换递归调用。这有效地对递归调用进行排序,现在它适用于任何大小和任何嵌套级别的数据。这种技术称为trampoline - 粗体变化

def identity (x):
  return x

def flatten (l):
  def loop (l, i = 0, path = (), acc = [], cont = identity):  
    if not l:
      return cont (acc)
    else:
      first, *rest = l
      if isinstance (first, list):
        return call (loop, first, 0, path + (i,), acc, lambda left:
          call (loop, rest, i + 1, path, [], lambda right:
            cont (left + right)))
      else:
        return call (loop, rest, i + 1, path, acc + [ (first, path + (i,)) ], cont)

  return loop (l) .run ()

class call:
  def __init__ (self, f, *xs):
    self.f = f
    self.xs = xs

  def run (self):
    acc = self
    while (isinstance (acc, call)):
      acc = acc.f (*acc.xs)
    return acc

print (flatten (L))
# [ (1, (0, 0, 0))
# , (2, (0, 0, 1))
# , (3, (0, 0, 2))
# , (4, (0, 1, 0))
# , (5, (0, 1, 1))
# , (6, (1, 0))
# , (7, (2, 0))
# , (8, (2, 1, 0))
# , (9, (2, 1, 1))
# , (10, (3,))
# ]

为什么更好?客观地说,这是一个更完整的计划。仅仅因为它似乎更复杂并不意味着效率更低。

当输入列表嵌套超过996级深度时(在python 3.x中),问题中提供的代码将失败

depth = 1000
L = [1]
while (depth > 0):
  L = [L]
  depth = depth - 1

for x in flatten (L):
  print (x)

# Bug in the question's code:
# the first value in the tuple is not completely flattened
# ([[[[[1]]]]], (0, 0, 0, ... ))

更糟糕的是,当depth增加到2000左右时,问题中提供的代码会生成运行时错误GeneratorExitException

使用我的程序时,它适用于任何大小的输入,嵌套到任何深度,并始终产生正确的输出。

depth = 50000
L = [1]
while (depth > 0):
  L = [L]
  depth = depth - 1

print (flatten (L))
# (1, (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49990 more...))

print (flatten (range (50000)))
# [ (0, (0,))
# , (1, (1,))
# , (2, (2,))
# , ...
# , (49999, (49999,))
# ]

无论如何谁会有如此深刻的名单?一个这样的常见情况是linked list,它创造了深层的树状结构

my_list = [ 1, [ 2, [ 3, [ 4, None ] ] ] ]

这种结构很常见,因为最外面的一对使我们可以轻松访问我们关心的两个语义部分:第一个项目和其他项目。链表也可以使用元组或字典来实现。

my_list = ( 1, ( 2, ( 3, ( 4, None ) ) ) )

my_list = { "first": 1
          , "rest": { "first": 2
                    , "rest": { "first": 3
                              , "rest": { "first": 4
                                        , "rest": None
                                        }
                              }
                    }
          }

在上面,我们可以看到一个明智的结构可能会创造一个重要的深度。在Python中,[](){}允许您无限地嵌套。为什么我们的通用flatten限制了这种自由?

我认为,如果你要设计像flatten这样的通用函数,我们应该选择在大多数情况下都能运行的实现,并且具有最少的惊喜。因为使用某种(深层)结构而突然失败的是糟糕的。在我的答案中使用的flatten并不是最快的[1],但它并没有给程序员带来奇怪的答案或程序崩溃感到惊讶。

[1]我没有衡量表现直到重要,所以我没有做任何调整上面的flatten。我的程序的另一个低调的优点是你可以调整它,因为我们写了它 - 另一方面,如果forenumerateyield在你的程序中引起问题,你会做什么来“修复”它?我们如何让它更快?我们如何使其适用于更大尺寸或更深的输入?法拉利缠绕在树上后有什么用呢?

另一答案

递归是扁平深层嵌套列表的好方法。您的实施也做得很好。我建议用这个similar recipe修改它如下:

<

以上是关于Python:使用索引展平嵌套列表的主要内容,如果未能解决你的问题,请参考以下文章

Python:展平多个嵌套的字典并追加

以独特的方式展平包含嵌套字典的列表的数据框列

在Python中展平一个浅表[重复]

如何使用两个列表推导展平嵌套的字符串列表

在 Erlang 中展平嵌套列表的列表

列表展平的时间复杂度