【问题标题】:Python3 functools lru_cache RuntimeErrorPython3 functools lru_cache RuntimeError
【发布时间】:2020-12-01 10:25:19
【问题描述】:
from functool import lru_cache


@lru_cache
def fibonacci(n):
    """0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    """

    if n == 0:
        yield 0
    elif n == 1:
        yield 1
    else:
        yield next(fibonacci(n - 1)) + next(fibonacci(n - 2))

如果我像这样使用 @lru_cache 装饰器调用此函数:

for x in range(10):
    print(next(fibonacci(x)))

我明白了:

StopIteration

The above exception was the direct cause of the following exception:

RuntimeError: generator raised StopIteration

我已经做了很多搜索,但我不知道如何解决这个问题。没有装饰器,一切正常。

【问题讨论】:

  • "我不知道如何解决这个问题。没有装饰器,一切正常。" - 听起来你确实知道如何解决这个问题。
  • 嗨,我想return 会比yield 更好here。 ;)
  • 您正在缓存生成器,每个生成器只会产生一个项目。它们将分别工作一次;稍后从缓存中检索其中一个将失败,因为它们没有剩余的项目。
  • 基本上代码战超时了,所以我想让它更快。认为这可以做到,但它不能,但删除装饰器并不能解决超时问题,所以我想知道为什么这不起作用。我需要能够一次只获得一个值,因为它需要能够从中间的不同函数中获取值,如果这是有意义的,然后从中断的地方继续,所以使用 return 会在内存中保留太多,因为我不知道传递给函数的数字有多大。

标签: python python-3.x runtime-error functools


【解决方案1】:

如果您确实想要缓存并因此重用生成器迭代器,请确保它们确实支持这一点。也就是说,让他们产生他们的结果,而不是一次,而是重复。例如:

@lru_cache
def fibonacci(n):
    """0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    """

    if n == 0:
        while True:
            yield 0
    elif n == 1:
        while True:
            yield 1
    else:
        result = next(fibonacci(n - 1)) + next(fibonacci(n - 2))
        while True:
            yield result

测试:

>>> for x in range(10):
        print(next(fibonacci(x)))

0
1
1
2
3
5
8
13
21
34

【讨论】:

  • 我不知道为什么你的似乎有效,而我的却没有。为什么会如此不同?
  • @SeaWolf 就像我说的那样,您正在尝试重用您的生成器迭代器,尝试从每个迭代器中多次获取某些东西,但每个迭代器只产生一次。因此,当您第二次询问时,它会失败。我的不会,因为我的生成器会根据要求一次又一次地产生结果。
  • 我不明白。它将如何摆脱 while 循环?我认为生成器函数基本上充当了一个while循环,只是它们一次返回一个值。没关系我猜。我只是绝望了。
  • @SeaWolf 它永远不会脱离循环。但它会产生。
【解决方案2】:

你可以使用记忆装饰器

参考:Can I memoize a Python generator? Jasmijn 的回答

代码

from itertools import tee
from types import GeneratorType

Tee = tee([], 1)[0].__class__

def memoized(f):
    cache={}
    def ret(*args):
        if args not in cache:
            cache[args]=f(*args)
        if isinstance(cache[args], (GeneratorType, Tee)):
            # the original can't be used any more,
            # so we need to change the cache as well
            cache[args], r = tee(cache[args])
            return r
        return cache[args]
    return ret

@memoized
def Fibonacci(n):
    """0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    """

    if n == 0:
        yield 0
    elif n == 1:
        yield 1
    else:
        yield next(fibonacci_mem(n - 1)) + next(fibonacci_mem(n - 2))

时序测试

总结

测试 n 从 1 到 20 orig:原始代码 lru:使用 lru 缓存 mem:使用记忆装饰器

每种算法运行 3 次的时间以秒为单位

结果表明 lru_cache 技术提供了最快的运行时间(即更短的时间)

n: 1 orig: 0.000008, lru 0.000006, mem: 0.000015
n: 10 orig: 0.000521, lru 0.000024, mem: 0.000057
n: 15 orig: 0.005718, lru 0.000013, mem: 0.000035
n: 20 orig: 0.110947, lru 0.000014, mem: 0.000040
n: 25 orig: 1.503879, lru 0.000018, mem: 0.000042

定时测试代码

from itertools import tee
from types import GeneratorType
from functools import lru_cache

Tee = tee([], 1)[0].__class__

def memoized(f):
    cache={}
    def ret(*args):
        if args not in cache:
            cache[args]=f(*args)
        if isinstance(cache[args], (GeneratorType, Tee)):
            # the original can't be used any more,
            # so we need to change the cache as well
            cache[args], r = tee(cache[args])
            return r
        return cache[args]
    return ret
    
def fibonacci(n):
    """0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    """

    if n == 0:
        yield 0
    elif n == 1:
        yield 1
    else:
        yield next(fibonacci(n - 1)) + next(fibonacci(n - 2))

@memoized
def fibonacci_mem(n):
    """0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    """

    if n == 0:
        yield 0
    elif n == 1:
        yield 1
    else:
        yield next(fibonacci_mem(n - 1)) + next(fibonacci_mem(n - 2))

@lru_cache
def fibonacci_cache(n):
    """0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    """

    if n == 0:
        while True:
            yield 0
    elif n == 1:
        while True:
            yield 1
    else:
        result = next(fibonacci_cache(n - 1)) + next(fibonacci_cache(n - 2))
        while True:
            yield result

from timeit import timeit

cnt = 3
for n in [1, 10, 15, 20, 25]:
  t_orig = timeit(lambda:next(fibonacci(n)), number = cnt)
  t_mem = timeit(lambda:next(fibonacci_mem(n)), number = cnt)
  t_cache = timeit(lambda:next(fibonacci_cache(n)), number = cnt)
  print(f'n: {n} orig: {t_orig:.6f}, lru {t_cache:.6f}, mem: {t_mem:.6f}')

【讨论】:

  • 啊,谢谢,这看起来像我需要的。我不知道你在做什么。我希望标准库有一个我可以装饰的记忆功能。我认为这就是我对@lru_cache 所做的。我明天试试这个。
  • @SeaWolf——有像 lru_cache 这样的标准记忆装饰器通常可以工作(我已经成功使用过很多次)。但是,递归生成器似乎是一种需要更专业方法的极端情况。如果您还有其他问题,请告诉我,我很乐意为您解释。
  • @superbrain——啊,你抓住了我。只是开玩笑:)。这就是为什么我包含我的计时代码,以便其他人可以发现我工作中的漏洞。我纠正了我的方式错误并更新了我的帖子。很高兴地说,现在看起来 lru_cache(您的帖子)提供了最快的时间(这更有意义)。
  • 是的,这也是我包含计时代码的原因之一(当我编写时)。顺便说一句,f = fibonacci_mem(5); next(f); next(f) 当然会给出一个错误(好吧,StopIteration)。所以next(fibonacci_mem(...)) 几乎是使用它的唯一方法。我认为单产量生成器没有意义,应该只是一个返回数字的“正常”函数,就像fibonacci_mem(...)一样使用。
猜你喜欢
  • 1970-01-01
  • 2018-09-27
  • 2016-12-20
  • 2016-02-13
  • 2018-04-23
  • 1970-01-01
  • 2023-03-13
  • 1970-01-01
  • 2021-01-29
相关资源
最近更新 更多