【发布时间】:2018-04-13 09:34:29
【问题描述】:
最近我正在写一个下载程序,它使用HTTP Range字段同时下载许多块。我写了一个 Python 类来表示 Range(HTTP 头的 Range 是一个闭区间):
class ClosedRange:
def __init__(self, begin, end):
self.begin = begin
self.end = end
def __iter__(self):
yield self.begin
yield self.end
def __str__(self):
return '[{0.begin}, {0.end}]'.format(self)
def __len__(self):
return self.end - self.begin + 1
__iter__魔术方法是支持元组解包:
header = {'Range': 'bytes={}-{}'.format(*the_range)}
而len(the_range) 是该范围内的字节数。
现在我发现'bytes={}-{}'.format(*the_range) 偶尔会导致MemoryError。经过一番调试,我发现CPython解释器在执行func(*iterable)时会尝试调用len(iterable),并且(可能)根据长度分配内存。在我的机器上,当len(the_range) 大于1GB 时,会出现MemoryError。
这是一个简化的:
class C:
def __iter__(self):
yield 5
def __len__(self):
print('__len__ called')
return 1024**3
def f(*args):
return args
>>> c = C()
>>> f(*c)
__len__ called
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError
>>> # BTW, `list(the_range)` have the same problem.
>>> list(c)
__len__ called
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError
所以我的问题是:
为什么 CPython 调用
len(iterable)?从this question 我看到你不会知道迭代器的长度,直到你迭代抛出它。这是优化吗?__len__方法能否返回对象的“假”长度(即不是内存中元素的实际数量)?
【问题讨论】:
-
python.org/dev/peps/pep-0424。我建议不要这样做(有一个不会产生范围元素的
__iter__)。 -
如果你的迭代器产生两个项目,那么你的序列长度就是两个。
-
@juanpa.arrivillaga @Ryan 我知道。
list(it)或f(*it)都创建一个序列,并会调用operator.length_hint(it)来预分配空间。而operator.length_hint看到it有__len__方法,所以只返回len(it)——序列分配太大。对吗?