[英]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.