簡體   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