Python生成器vs迭代器

du7egjpx  于 2023-04-04  发布在  Python
关注(0)|答案(1)|浏览(145)

所以我有一个自定义迭代器,它表示为下面的一个类CustomIterator。最后一个打印显示了它从某个字符串中获取一个字符时使用的所有数据的大小。1170字节。

class CustomIterator:
    def __init__(self, collection: str):
        self.__position = 0
        self.__collection = collection

    def __iter__(self):  
        return self

    def __next__(self):   
        while self.__position < len(self.__collection):  
            char = self.__collection[self.__position]    
            self.__position += 1                         
            return char.upper()                         
        raise StopIteration                            

iterator = CustomIterator(s)                            
print(f'{sys.getsizeof(CustomIterator) + sys.getsizeof(iterator) + sys.getsizeof(next(iterator))}') #1170

我也有一个生成器,它表示为下面的yield操作符的函数。这里最后一个打印的意思和迭代器相同。154字节。

#Generator
def generator(s: str):
    for char in s:
        yield char.upper()

g = generator(s)
print(f'{sys.getsizeof(generator(s)) + sys.getsizeof(next(g))}') #154

这两段代码产生了相同的结果。那么Python中的yield操作符到底是如何工作的呢?我假设它从基类迭代器继承next方法并覆盖它。这是正确的吗?如果是这样,这两段代码的资源需求一定是相同的?
我试图在谷歌的文档和一些文章中找到答案

nzk0hqpo

nzk0hqpo1#

你在几个方面都是正确的,包括当你说“如果是这样的话,两个代码的资源需求一定是相同的?”-是的,两种形式将使用基本相同的资源,并且一个是否碰巧更高性能实际上更多地是由于实现细节而不是任何基本的东西。
例如,由于编写为实现__next__的类的纯Python迭代器需要Python函数调用,因此在Python 3.10之前,它们可能比使用yield的生成器函数慢,但在Python 3.11上不一定如此,因为函数调用的开销减少了。pipy实现不应该在用户在__next__方法中编写的Python代码和运行时为生成器函数执行的内部代码之间存在差异。
在一个理想的世界里,它们的表现是一样的,而且在任何(合理的)Python实现中,这两种形式的“大O”算法因素肯定是一样的。
这给我们留下了形式上的差异:事实上,当一个函数在其主体中包含yield关键字时(即使它在代码的不可访问部分),该函数在编译时被创建为“生成器函数”:这意味着当它被调用时,其中的任何可见代码都不会被执行。相反,Python创建了一个返回的“生成器对象”的示例。该对象具有__next__sendthrow方法,这些方法随后可用于驱动生成器:在这个意义上,生成器与用户实现的迭代器工作相同。
至于sys.getsizeof的输出,这当然是一件不应该关心的事情。这个函数的输出不是一个可靠的度量,因为它不会显示任何引用对象的值。例如,用户类的示例通常会有一个关联的完整大小的字典(尽管这在最近的cPython版本中也进行了优化)。总而言之,生成器函数创建的生成器所使用的总字节数的差异和一个用户类创建的迭代器,甚至可能有几百个字节,有利于生成器函数1:但这在大多数工作流中不会产生任何差异,除非创建数百个(对于大型服务器进程,数万个)并行使用的生成器(即,在旧的生成器被使用并从内存中删除之前创建新的生成器)。
即使是它们,用户类也可以被优化(使用__slots__和其他技术)。
在你的比较中,特别是:

print(f'{sys.getsizeof(CustomIterator) + sys.getsizeof(iterator) + sys.getsizeof(next(iterator))}') #1170

你得到的是class对象本身的大小-它是type的示例,当然会使用更多的内存(sys.getsizeof(CustomIterator))-你看到的1000个额外字节并不多:这个数量只创建一次(),并且在进程的生命周期内一直使用。2每个迭代器示例将使用另一个数量的内存,当迭代器不再使用时,这些内存将被释放。
至于generator-function创建的generator的内部状态,这似乎是你关心的另一件事:它当然不是魔术-它被维护在一个甚至可以内省的对象中,称为“执行帧”。当您调用生成器函数时,返回的“生成器对象”具有.gi_frame属性,并且您可以检查.gi_frame.f_locals处的内部局部变量。同样的状态保持以嵌套的方式发生,当你运行for char in s:时。不同之处在于for语句在s上创建了一个迭代器,它不能从Python代码中直接访问。但是你可以这样做:iter_s = iter(s); for char in iter_s,并在iter_s对象中看到一些您想要的状态(这不会暴露内部状态,就像Python中用作计数器的变量一样,但是__next__方法在那里。
)如果你碰巧把你的“class”语句,连同它的主体和所有内容,放在一个循环或函数中,它将在每次运行时再次执行,但这将是不正确的代码。

相关问题