繁体   English   中英

用生成器定义“fac”。 并且:为什么生成器没有堆栈溢出?

[英]Definining `fac` with generators. And: Why no stack overflow with generators?

有没有一种方法可以通过 Python 中的生成器定义以下代码(递归的经典示例)? 我正在使用 Python 3。

def fac(n):
    if n==0:
        return 1
    else:
        return n * fac(n-1)

我试过这个,没有成功:

In [1]: def fib(n):
   ...:     if n == 0:
   ...:         yield 1
   ...:     else:
   ...:         n * yield (n-1)
  File "<ipython-input-1-bb0068f2d061>", line 5
    n * yield (n-1)
            ^
SyntaxError: invalid syntax

Python 中的经典递归导致堆栈溢出

对于n=3000的输入,这个经典示例在我的机器上导致堆栈溢出。 在 Lisp 方言“方案”中,我会使用尾递归并避免堆栈溢出。 在 Python 中不可能。 这就是生成器在 Python 中派上用场的原因。 但我想知道:

为什么生成器没有堆栈溢出?

为什么 Python 中的生成器没有堆栈溢出? 他们如何在内部工作? 做一些研究总是让我看到一些例子,展示如何在 Python 中使用生成器,但对内部工作的了解不多。

更新 1: yield from my_function(...)

正如我试图在评论部分解释的那样,也许我上面的例子是一个糟糕的选择。 我的实际问题是针对在 Python 3 中yield from语句中递归使用的生成器的内部工作原理。

下面是一个(不完整的)示例代码,我用来处理由 Firebox 书签备份生成的 JSON 文件。 在几个点上,我使用yield from process_json(...)的 yield 通过生成器再次递归调用 function。

正是在这个例子中,如何避免堆栈溢出? 或者是吗?


# (snip)

FOLDERS_AND_BOOKMARKS = {}
FOLDERS_DATES = {}

def process_json(json_input, folder_path=""):
    global FOLDERS_AND_BOOKMARKS
    # Process the json with a generator
    # (to avoid recursion use generators)
    # https://stackoverflow.com/a/39016088/5115219

    # Is node a dict?
    if isinstance(json_input, dict):
        # we have a dict
        guid = json_input['guid']
        title = json_input['title']
        idx = json_input['index']
        date_added = to_datetime_applescript(json_input['dateAdded'])
        last_modified = to_datetime_applescript(json_input['lastModified'])

        # do we have a container or a bookmark?
        #
        # is there a "uri" in the dict?
        #    if not, we have a container
        if "uri" in json_input.keys():
            uri = json_input['uri']
            # return URL with folder or container (= prev_title)
            # bookmark = [guid, title, idx, uri, date_added, last_modified]
            bookmark = {'title': title,
                        'uri':   uri,
                        'date_added': date_added,
                        'last_modified': last_modified}
            FOLDERS_AND_BOOKMARKS[folder_path].append(bookmark)
            yield bookmark

        elif "children" in json_input.keys():
            # So we have a container (aka folder).
            #
            # Create a new folder
            if title != "": # we are not at the root
                folder_path = f"{folder_path}/{title}"
                if folder_path in FOLDERS_AND_BOOKMARKS:
                    pass
                else:
                    FOLDERS_AND_BOOKMARKS[folder_path] = []
                    FOLDERS_DATES[folder_path] = {'date_added': date_added, 'last_modified': last_modified}

            # run process_json on list of children
            # json_input['children'] : list of dicts
            yield from process_json(json_input['children'], folder_path)

    # Or is node a list of dicts?
    elif isinstance(json_input, list):
        # Process children of container.
        dict_list = json_input
        for d in dict_list:
            yield from process_json(d, folder_path)

更新 2: yieldyield from

好的我明白了。 感谢所有的评论。

  • 所以生成器通过yield创建迭代器。 这与递归无关,所以这里没有堆栈溢出。
  • 但是通过yield from my_function(...)生成器确实是我的 function 的递归调用,尽管延迟了,并且仅在需要时才进行评估。

第二个示例确实会导致堆栈溢出。

好的,在您发表评论后,我已经完全重写了我的答案。

  1. 递归是如何工作的,为什么会出现堆栈溢出?

递归通常是解决问题的一种优雅方式。 在大多数编程语言中,每次调用 function 时,function 所需的所有信息和 state 都会放入堆栈 - 所谓的“堆栈帧”。 堆栈是一个特殊的每线程 memory 区域并且大小有限。

现在递归函数隐式使用这些堆栈帧来存储状态/中间结果。 例如,阶乘 function 是 n * (n-1) * ((n-1) -1)... 1 并且所有这些“n-1”都存储在堆栈中。

迭代解决方案必须将这些中间结果显式存储在变量中(通常位于单个堆栈帧中)。

  1. 生成器如何避免堆栈溢出?

简单地说:它们不是递归的。 它们像迭代器对象一样实现。 它们存储计算的当前 state 并在您每次请求时返回一个新结果(隐式或使用 next())。

如果它看起来是递归的,那只是语法糖。 “收益”不像回报。 它产生当前值,然后“暂停”计算。 这一切都包含在一个 object 中,而不是在数以千计的堆栈帧中。

这将为您提供从 '1 到 n:' 的系列:

def fac(n):
    if (n <= 0):
        yield 1
    else:
        v = 1
        for i in range(1, n+1):
            v = v * i
            yield v

没有递归,中间结果存储在v中,它很可能存储在一个 object 中(可能在堆上)。

  1. yield from如何

好的,这很有趣,因为它仅在 Python 3.3 中添加。 yield from可用于委托给另一个生成器。

你举了一个例子:

def process_json(json_input, folder_path=""):
    # Some code
    yield from process_json(json_input['children'], folder_path)

这看起来是递归的,但实际上它是两个生成器对象的组合。 你有你的“内部”生成器(它只使用一个对象的空间)并且你说“我想将该yield from器中的所有值转发给我的调用者”。

因此,它不会为每个生成器结果生成一个堆栈帧,而是为每个使用的生成器创建一个 object。

在此示例中,您将为每个子 JSON 对象创建一个生成器 object。 如果您以递归方式执行,那可能需要相同数量的堆栈帧。 但是,您不会看到堆栈溢出,因为对象是在堆上分配的,并且那里的大小限制非常不同 - 取决于您的操作系统和设置。 在我的笔记本电脑上,使用 Ubuntu Linux, ulimit -s给我 8 MB 的默认堆栈大小,而我的进程 memory 的内存大小是无限的(虽然我只有 8GB 的物理内存)。

查看有关生成器的文档页面: https://wiki.python.org/moin/Generators

还有这个 QA: 了解 Python 中的生成器

一些很好的例子,也yield fromhttps://www.python-course.eu/python3_generators.php

TL;DR:生成器是对象,它们不使用递归。 甚至没有yield from ,它只是委托给另一个生成器 object。 递归仅在调用数量有限且很小,或者您的编译器支持尾调用优化时才实用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM