【问题标题】:Caching a generator缓存生成器
【发布时间】:2013-10-30 11:20:28
【问题描述】:

最近的一个类似问题 (isinstance(foo, types.GeneratorType) or inspect.isgenerator(foo)?) 让我对如何通用地实现它感到好奇。

实际上,拥有一个生成器类型的对象似乎是一件普遍有用的事情,该对象将在第一次通过时进行缓存(如itertools.cycle),报告 StopIteration,然后在下一次通过时从缓存中返回项目,但如果对象不是生成器(即固有地支持 O(1) 查找的列表或字典),则不要缓存,并且具有相同的行为,但对于原始列表。

可能性:

1) 修改itertools.cycle。它看起来像这样:

def cycle(iterable):
    saved = []
    try: 
         saved.append(iterable.next())
         yield saved[-1]
         isiter = True
    except:
         saved = iterable
         isiter = False
    # cycle('ABCD') --> A B C D A B C D A B C D ...
    for element in iterable:
        yield element
        if isiter: 
            saved.append(element)

     # ??? What next?

如果我可以重新启动生成器,那就太完美了——我可以发回一个 StopIteration,然后在下一个 gen.next() 上,返回条目 0,即“ABCD StopIteration ABCD StopIteration”,但它看起来不像这实际上是可能的。

其次是一旦 StopIteration 被击中,然后保存有一个缓存。但看起来没有任何方法可以访问内部 saved[] 字段。也许这是一个班级版本?

2) 或者我可以直接传入列表:

def cycle(iterable, saved=[]):
    saved.clear()
    try: 
         saved.append(iterable.next())
         yield saved[-1]
         isiter = True
    except:
         saved = iterable
         isiter = False
    # cycle('ABCD') --> A B C D A B C D A B C D ...
    for element in iterable:
        yield element
        if isiter: 
            saved.append(element)

mysaved = []
myiter = cycle(someiter, mysaved)

但这看起来很糟糕。而在 C/++ 中,我可以传入一些引用,并将实际引用更改为已保存以指向可迭代 - 你实际上不能在 python 中这样做。所以这甚至行不通。

其他选择?

编辑:更多数据。 CachingIterable 方法似乎太慢而无法有效,但它确实将我推向了一个可行的方向。它比天真的方法(转换为列出我自己)稍慢,但如果它已经是可迭代的,它似乎不会受到打击。

一些代码和数据:

def cube_generator(max=100):
    i = 0
    while i < max:
        yield i*i*i
        i += 1

# Base case: use generator each time
%%timeit
cg = cube_generator(); [x for x in cg]
cg = cube_generator(); [x for x in cg]
cg = cube_generator(); [x for x in cg]
10000 loops, best of 3: 55.4 us per loop

# Fastest case: flatten to list, then iterate
%%timeit
cg = cube_generator()
cl = list(cg)
[x for x in cl]
[x for x in cl]
[x for x in cl]
10000 loops, best of 3: 27.4 us per loop

%%timeit
cg = cube_generator()
ci2 = CachingIterable(cg)
[x for x in ci2]
[x for x in ci2]
[x for x in ci2]
1000 loops, best of 3: 239 us per loop

# Another attempt, which is closer to the above
# Not exactly the original solution using next, but close enough i guess
class CacheGen(object):
    def __init__(self, iterable):
        if isinstance(iterable, (list, tuple, dict)):
            self._myiter = iterable
        else:
            self._myiter = list(iterable)
    def __iter__(self):
        return self._myiter.__iter__()
    def __contains__(self, key):
        return self._myiter.__contains__(key)
    def __getitem__(self, key):
        return self._myiter.__getitem__(key)

%%timeit
cg = cube_generator()
ci = CacheGen(cg)
[x for x in ci]
[x for x in ci]
[x for x in ci]
10000 loops, best of 3: 30.5 us per loop

# But if you start with a list, it is faster
cg = cube_generator()
cl = list(cg)
%%timeit
[x for x in cl]
[x for x in cl]
[x for x in cl]
100000 loops, best of 3: 11.6 us per loop

%%timeit
ci = CacheGen(cl)
[x for x in ci]
[x for x in ci]
[x for x in ci]
100000 loops, best of 3: 13.5 us per loop

任何更快的食谱可以更接近“纯”循环?

【问题讨论】:

  • 主要问题是一旦StopIteration 被提出,那么根据生成器规范,它应该不再产生任何东西......
  • 是的,这正是我的问题。我只是想要一些你可以迭代的东西,但我想一个可迭代的作品也一样。顺便说一句,我意识到采用一个包装列表的类会有点简单,返回 list.iter 作为它自己的 iter,如果你传递一个生成器,只需用 list(generator) 解开它并做同样的事情。
  • 为什么扁平化案例在早期每个循环需要 23.5 us,但之后每个循环需要 11.6 us?您是否在相同的稳定环境中进行测试?
  • 我没有看到 23.5,但如果您的意思是 27.4 与 11.6,则 27.4 是从生成器创建列表并迭代列表 3 次的时机; 11.6 仅用于迭代列表 3 次。它只是为了表明这个 CacheGen 实现不会复制列表,如果它得到一个,只有当它得到一个生成器时。
  • @CorleyBrigman:好的,明白了,这是有道理的。所以是的,似乎任何解决方案都比只做list() 然后遍历列表要慢 - 所以你的CacheGen 将是要走的路。如果最终您必须用尽整个迭代器,那么您不妨一开始就一次性完成所有操作。但是,如果您有无限的生成器,那么您将无法那样做。或者如果你可能不迭代整个事情,你会浪费资源。我已经使用更高效的“随用随用”缓存器更新了我的答案,但仍然比简单的缓存器慢

标签: python performance caching generator iterable


【解决方案1】:

基于此评论:

我的意图是,只有当用户知道他想对“可迭代”进行多次迭代,但不知道输入是生成器还是可迭代时,才使用此方法。这可以让您忽略这种区别,同时不会损失(很多)性能。

这个简单的解决方案正是这样做的:

def ensure_list(it):
    if isinstance(it, (list, tuple, dict)):
        return it
    else:
        return list(it)

现在 ensure_list(a_list) 实际上是一个无操作 - 两个函数调用 - 而 ensure_list(a_generator) 会将其转换为一个列表并返回它,结果证明这比任何其他方法都快。

【讨论】:

    【解决方案2】:

    你想要的不是一个迭代器,而是一个可迭代对象。迭代器只能遍历其内容一次。你想要一个带有迭代器的东西,然后你可以在上面迭代多次,从迭代器中产生相同的值,即使迭代器不记得它们,比如生成器。然后,只需对那些不需要缓存的输入进行特殊处理。这是一个非线程安全的示例(编辑:为提高效率而更新):

    import itertools
    class AsYouGoCachingIterable(object):
        def __init__(self, iterable):
            self.iterable = iterable
            self.iter = iter(iterable)
            self.done = False
            self.vals = []
    
        def __iter__(self):
            if self.done:
                return iter(self.vals)
            #chain vals so far & then gen the rest
            return itertools.chain(self.vals, self._gen_iter())
    
        def _gen_iter(self):
            #gen new vals, appending as it goes
            for new_val in self.iter:
                self.vals.append(new_val)
                yield new_val
            self.done = True
    

    还有一些时间安排:

    class ListCachingIterable(object):
        def __init__(self, obj):
            self.vals = list(obj)
    
        def __iter__(self):
            return iter(self.vals)
    
    def cube_generator(max=1000):
        i = 0
        while i < max:
            yield i*i*i
            i += 1
    
    def runit(iterable_factory):
        for i in xrange(5):
            for what in iterable_factory():
                pass
    
    def puregen():
        runit(lambda: cube_generator())
    def listtheniter():
        res = list(cube_generator())
        runit(lambda: res)
    def listcachingiterable():
        res = ListCachingIterable(cube_generator())
        runit(lambda: res)
    def asyougocachingiterable():
        res = AsYouGoCachingIterable(cube_generator())
        runit(lambda: res)
    

    结果是:

    In [59]: %timeit puregen()
    1000 loops, best of 3: 774 us per loop
    
    In [60]: %timeit listtheniter()
    1000 loops, best of 3: 345 us per loop
    
    In [61]: %timeit listcachingiterable()
    1000 loops, best of 3: 348 us per loop
    
    In [62]: %timeit asyougocachingiterable()
    1000 loops, best of 3: 630 us per loop
    

    因此,就类而言,最简单的方法ListCachingIterable 与手动执行list 一样有效。 “as-you-go”变体的速度几乎是原来的两倍,但如果您不使用整个列表,则具有优势,例如假设您只寻找第一个超过 100 的立方体:

    def first_cube_past_100(cubes):
        for cube in cubes:
            if cube > 100:
                return cube
        raise Error("No cube > 100 in this iterable")
    

    然后:

    In [76]: %timeit first_cube_past_100(cube_generator())
    100000 loops, best of 3: 2.92 us per loop
    
    In [77]: %timeit first_cube_past_100(ListCachingIterable(cube_generator()))
    1000 loops, best of 3: 255 us per loop
    
    In [78]: %timeit first_cube_past_100(AsYouGoCachingIterable(cube_generator()))
    100000 loops, best of 3: 10.2 us per loop
    

    【讨论】:

    • 这看起来很合理,我会考虑这个,看看它是否完全解决了我的问题。非缓存有时是一个问题,但一个例子可能是加入,它将通过列表两次,而不是被修改。标准程序是传递一个列表(为了性能),但如果它已经是一个列表,则不一定要复制它 - 你可以做类似 ''.join(CachingIterable(my_real_iterable)) 的事情,它会是“自动的”......
    • 嗯,我不认为我可以接受这个答案......主要是因为它对于少量迭代来说非常慢 - 做 3 次,它比仅仅慢 5 倍使用没有缓存的生成器。也许是一种优化的方法?
    • @CorleyBrigman:嗯,也许,你能把你的测试用例的键盘或粘贴箱放在一边,这样我就可以搞砸了吗?
    • 只是想感谢您提供额外的时间细节。并且 AsYouGoCachingIterable 可能会通过将 self.vals.append(new_val) 替换为 self.vals += (new_val,) 来稍微加快速度...
    • 可能不完全...但是如果设置l1 = range(100); l2 = [101] (python 2.6),然后执行%%timeit l1 += l2%%timeit l1.extend(l2),+= 版本大约快20%。没有直接关系,是的,但是 %timeit 表明使用元组执行 += 比 append 快 15%。
    【解决方案3】:

    刚刚创建了一个library,它正好解决了这个问题——支持对返回迭代器的函数进行缓存:

    from typing import *
    from cacheable_iter import iter_cache
    
    @iter_cache
    def iterator_function(n: int) -> Iterator[int]:
        yield from range(n)
    

    使用示例:

    from typing import *
    from cacheable_iter import iter_cache
    
    @iter_cache
    def my_iter(n: int) -> Iterator[int]:
        print(" * my_iter called")
        for i in range(n):
            print(f" * my_iter step {i}")
            yield i
    
    gen1 = my_iter(4)
    print("Creating an iterator...")
    print(f"The first value of gen1 is {next(gen1)}")
    print(f"The second value of gen1 is {next(gen1)}")
    
    gen2 = my_iter(4)
    print("Creating an iterator...")
    print(f"The first value of gen2 is {next(gen2)}")
    print(f"The second value of gen2 is {next(gen2)}")
    print(f"The third value of gen2 is {next(gen2)}")
    

    哪个会打印:

    Creating an iterator...
     * my_iter called
     * my_iter step 0
    The first value of gen1 is 0
     * my_iter step 1
    The second value of gen1 is 1
    Creating an iterator...
    The first value of gen2 is 0
    The second value of gen2 is 1
     * my_iter step 2
    The third value of gen2 is 2
    

    还支持缓存等待迭代器和异步迭代器

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-05
      • 1970-01-01
      • 2022-08-06
      • 1970-01-01
      相关资源
      最近更新 更多