最近我正在编写一个下载程序,它使用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解释器在执行len(iterable)
时会尝试调用func(*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__
方法可以返回对象的'假'长度(即不是内存中元素的实际数量)吗?
答案 0 :(得分:2)
为什么CPython会调用
len(iterable)
?从这个问题我看到你不知道迭代器的长度,直到你迭代扔它。这是优化吗?
当python(假设python3)执行f(*c)
时,使用了操作码CALL_FUNCTION_EX
:
0 LOAD_GLOBAL 0 (f)
2 LOAD_GLOBAL 1 (c)
4 CALL_FUNCTION_EX 0
6 POP_TOP
因为c
是可迭代的,所以调用PySequence_Tuple
将其转换为元组,然后调用PyObject_LengthHint
来确定新的元组长度,因为__len__
方法是在c
上定义,它被调用,其返回值用于为新元组分配内存,因为malloc
失败,最终引发MemoryError
错误。
/* Guess result size and allocate space. */
n = PyObject_LengthHint(v, 10);
if (n == -1)
goto Fail;
result = PyTuple_New(n);
__len__
方法可以返回“假冒”字样。对象的长度(即不是内存中元素的实际数量)?
在这种情况下,是的。
当__len__
的返回值小于需要时,python将在填充元组时调整新元组对象的内存空间。如果它大于需要,虽然python将分配额外的内存,但最终将调用_PyTuple_Resize
来回收过度分配的空间。