【问题标题】:Recursive generator for flattening nested lists用于展平嵌套列表的递归生成器
【发布时间】:2012-07-07 19:09:29
【问题描述】:

我是一名编程新手,在理解我的 Python 教科书(Magnus Lie Hetland 的“Beginning Python”)中的一个示例时遇到了一些困难。该示例用于递归生成器,旨在展平嵌套列表的元素(具有任意深度):

def flatten(nested):
    try:
        for sublist in nested:
            for element in flatten(sublist):
                yield element
    except TypeError:
        yield nested

然后您将按如下方式输入嵌套列表:

>>> list(flatten([[[1],2],3,4,[5,[6,7]],8]))
[1,2,3,4,5,6,7,8]

我了解 flatten() 中的递归如何帮助减少到此列表的最里面的元素“1”,但我不明白的是当“1”实际上传递回 flatten() 时会发生什么作为“嵌套”。我认为这会导致 TypeError (不能迭代一个数字),并且异常处理实际上会为生成输出做繁重的工作......但是使用 flatten() 的修改版本进行测试让我确信事实并非如此。相反,'yield element' 行似乎是负责任的。

也就是说,我的问题是……“yield 元素”如何真正执行?看起来“嵌套”要么是一个列表——在这种情况下会添加另一层递归——或者它是一个数字,你会得到一个 TypeError。

对此的任何帮助将不胜感激...特别是,我很想了解事件链,因为 flatten() 处理了一个简单的示例,例如:

list(flatten([[1,2],3]))

【问题讨论】:

  • 我建议阅读The Python yield keyword explained 的答案,以便很好地介绍所有相关概念。
  • @SvenMarnach:3.3 中的yield from 功能将使这成为小菜一碟:)
  • 附注:上面的代码不够通用,无法处理字符串。例如,list(flatten(['abc', 'def'])) 中断。

标签: python recursion


【解决方案1】:

也许您的部分困惑是您将最终的yield 语句视为return 语句。实际上,有几个人建议,当在此代码中抛出 TypeError 时,传递的项目是“返回”的。事实并非如此!

请记住,任何时候yield 出现在函数中,结果都不是单个项目,而是一个可迭代 -- 即使序列中只出现一个项目。因此,当您将1 传递给flatten 时,结果是单项生成器。要从中取出项目,您仍然需要对其进行迭代。

由于这个单项生成器是可迭代的,因此当内部 for 循环尝试对其进行迭代时,它不会抛出 TypeError;但是内部的for 循环只执行一次。然后外部的for 循环移动到嵌套列表中的下一个可迭代对象。

另一种思考方式是说,每次您将不可迭代的值传递给flatten 时,它都会将该值包装在一个可迭代的单项中并“返回”它。

【讨论】:

    【解决方案2】:

    分解您通常理解的函数的一种好方法是使用 python 调试器。这里添加了 cmets:

    -> def flatten(nested):
    (Pdb) l
      1  -> def flatten(nested):
      2         try:
      3             for sublist in nested:
      4                 for element in flatten(sublist):
      5                     yield element
      6         except TypeError:
      7             yield nested
      8     
      9     import pdb; pdb.set_trace()
     10     list(flatten([[1,2],3]))
     11     
    (Pdb) a
    nested = [[1, 2], 3]
    

    上面,我们刚刚输入了函数,参数是[[1, 2], 3]。让我们使用 pdb 的 step 函数单步执行该函数,进入我们应该遇到的任何递归调用:

    (Pdb) s
    > /Users/michael/foo.py(2)flatten()
    -> try:
    (Pdb) s
    > /Users/michael/foo.py(3)flatten()
    -> for sublist in nested:
    (Pdb) s
    > /Users/michael/foo.py(4)flatten()
    -> for element in flatten(sublist):
    (Pdb) s
    --Call--
    > /Users/michael/foo.py(1)flatten()
    -> def flatten(nested):
    (Pdb) a
    nested = [1, 2]
    

    我们已经进入了flatten 的一个内框,其中的参数是[1, 2]

    (Pdb) s
    > /Users/michael/foo.py(2)flatten()
    -> try:
    (Pdb) s
    > /Users/michael/foo.py(3)flatten()
    -> for sublist in nested:
    (Pdb) s
    > /Users/michael/foo.py(4)flatten()
    -> for element in flatten(sublist):
    (Pdb) s
    --Call--
    > /Users/michael/foo.py(1)flatten()
    -> def flatten(nested):
    (Pdb) a
    nested = 1
    

    两帧后,参数1 不再是可迭代的。这应该很有趣……

    (Pdb) s
    > /Users/michael/foo.py(2)flatten()
    -> try:
    (Pdb) s
    > /Users/michael/foo.py(3)flatten()
    -> for sublist in nested:
    (Pdb) s
    TypeError: "'int' object is not iterable"
    > /Users/michael/foo.py(3)flatten()
    -> for sublist in nested:
    (Pdb) s
    > /Users/michael/foo.py(6)flatten()
    -> except TypeError:
    (Pdb) s
    > /Users/michael/foo.py(7)flatten()
    -> yield nested
    (Pdb) s
    --Return--
    > /Users/michael/foo.py(7)flatten()->1
    -> yield nested
    

    好的,因为except TypeError,我们只是产生了参数本身。上一帧!

    (Pdb) s
    > /Users/michael/foo.py(5)flatten()
    -> yield element
    (Pdb) l
      1     def flatten(nested):
      2         try:
      3             for sublist in nested:
      4                 for element in flatten(sublist):
      5  ->                 yield element
      6         except TypeError:
      7             yield nested
      8     
      9     import pdb; pdb.set_trace()
     10     list(flatten([[1,2],3]))
     11     
    

    yield element 当然会产生1,所以一旦我们的最低帧命中TypeError,结果就会一直传播到堆栈的最外层帧flatten,这会将它产生给外部世界在继续到外部可迭代的其他部分之前。

    【讨论】:

      【解决方案3】:

      如果nested 是一个列表但sublist 不是,则可以执行yield element(即,如果nested 是一个普通的“平面”列表)。在这种情况下,for sublist in nested 可以正常工作。当下一行递归调用flatten sublist 时,当递归调用尝试迭代“子列表”(不可迭代)时,将引发类型错误。这个 TypeError 被捕获,递归调用返回整个输入列表,因此它被for element in flatten(sublist) 调用迭代。换句话说,for element in flatten(sublist) 最终会在子列表已经平坦的情况下执行 for element in sublist

      要认识到的关键是,即使是非嵌套列表也会导致递归调用。像flatten([1]) 这样的调用将导致两个 产生:递归调用将产生[1] 给外部调用,而外部调用立即重新产生1

      这个版本的函数可能有助于理解发生了什么:

          def flatten(nested, indent=""):
              try:
                  print indent, "Going to iterate over", nested
                  for sublist in nested:
                      print indent, "Going to iterate over flattening of", sublist
                      for element in flatten(sublist, indent+"  "):
                          print indent, "Yielding", element
                          yield element
              except TypeError:
                  print indent, "Type Error!  Yielding", nested
                  yield nested
      
          >>> list(flatten([[1,2],3]))
           Going to iterate over [[1, 2], 3]
           Going to iterate over flattening of [1, 2]
             Going to iterate over [1, 2]
             Going to iterate over flattening of 1
               Going to iterate over 1
               Type Error!  Yielding 1
             Yielding 1
           Yielding 1
             Going to iterate over flattening of 2
               Going to iterate over 2
               Type Error!  Yielding 2
             Yielding 2
           Yielding 2
           Going to iterate over flattening of 3
             Going to iterate over 3
             Type Error!  Yielding 3
           Yielding 3
          [1, 2, 3]
      

      【讨论】:

        【解决方案4】:

        我在函数中添加了一些工具:

        def flatten(nested, depth=0):
            try:
                print("{}Iterate on {}".format('  '*depth, nested))
                for sublist in nested:
                    for element in flatten(sublist, depth+1):
                        print("{}got back {}".format('  '*depth, element))
                        yield element
            except TypeError:
                print('{}not iterable - return {}'.format('  '*depth, nested))
                yield nested
        

        正在调用

        list(flatten([[1,2],3]))
        

        展示

        Iterate on [[1, 2], 3]
          Iterate on [1, 2]
            Iterate on 1
            not iterable - return 1
          got back 1
        got back 1
            Iterate on 2
            not iterable - return 2
          got back 2
        got back 2
          Iterate on 3
          not iterable - return 3
        got back 3
        

        【讨论】:

        • 如果任何元素是字符串则失败,因为它本身是可迭代的。
        【解决方案5】:

        tryexcept 构造会为您捕获异常并返回 nested,这正是提供给 flatten() 的参数。

        所以 flatten(1) 将在 for sublist in nested: 中出错并继续使用 except 部分并产生 nested1

        【讨论】:

          猜你喜欢
          • 2018-06-24
          • 2013-11-17
          • 2017-05-08
          • 2022-12-11
          • 2015-01-28
          • 2011-03-09
          • 1970-01-01
          • 1970-01-01
          • 2020-11-23
          相关资源
          最近更新 更多