[英]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
對於n=3000
的輸入,這個經典示例在我的機器上導致堆棧溢出。 在 Lisp 方言“方案”中,我會使用尾遞歸並避免堆棧溢出。 在 Python 中不可能。 這就是生成器在 Python 中派上用場的原因。 但我想知道:
為什么 Python 中的生成器沒有堆棧溢出? 他們如何在內部工作? 做一些研究總是讓我看到一些例子,展示如何在 Python 中使用生成器,但對內部工作的了解不多。
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)
yield
與yield from
好的我明白了。 感謝所有的評論。
yield
創建迭代器。 這與遞歸無關,所以這里沒有堆棧溢出。yield from my_function(...)
生成器確實是我的 function 的遞歸調用,盡管延遲了,並且僅在需要時才進行評估。第二個示例確實會導致堆棧溢出。
好的,在您發表評論后,我已經完全重寫了我的答案。
遞歸通常是解決問題的一種優雅方式。 在大多數編程語言中,每次調用 function 時,function 所需的所有信息和 state 都會放入堆棧 - 所謂的“堆棧幀”。 堆棧是一個特殊的每線程 memory 區域並且大小有限。
現在遞歸函數隱式使用這些堆棧幀來存儲狀態/中間結果。 例如,階乘 function 是 n * (n-1) * ((n-1) -1)... 1 並且所有這些“n-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 中(可能在堆上)。
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 from
: https://www.python-course.eu/python3_generators.php
TL;DR:生成器是對象,它們不使用遞歸。 甚至沒有yield from
,它只是委托給另一個生成器 object。 遞歸僅在調用數量有限且很小,或者您的編譯器支持尾調用優化時才實用。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.