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