为什么 CPython 在执行 func(*iterable) 时调用 len(iterable)?
Why CPython call len(iterable) when executing func(*iterable)?
最近在写一个下载程序,利用HTTP Range字段同时下载很多块。我写了一个Python class来表示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 我看到在迭代 throw 之前你不会知道迭代器的长度。这是优化吗?
可以__len__
方法return对象的'fake'长度(即不是内存中元素的实际数量)吗?
Why CPython call len(iterable)
? From this question I see you won't know an iterator's length until you iterate throw it. Is this an optimization?
当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
上定义,它被调用并且其 return 值用于为新元组分配内存,因为 malloc
失败,最终引发 MemoryError
错误。
/* Guess result size and allocate space. */
n = PyObject_LengthHint(v, 10);
if (n == -1)
goto Fail;
result = PyTuple_New(n);
Can __len__
method return the 'fake' length (i.e. not the real number of elements in memory) of an object?
在这种情况下,是的。
当__len__
的return值小于需要时,python会在填充元组时调整新元组对象的内存space以适应。如果它比需要的大,虽然 python 会分配额外的内存,但最后会调用 _PyTuple_Resize
来回收过度分配的 space.
最近在写一个下载程序,利用HTTP Range字段同时下载很多块。我写了一个Python class来表示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 我看到在迭代 throw 之前你不会知道迭代器的长度。这是优化吗?可以
__len__
方法return对象的'fake'长度(即不是内存中元素的实际数量)吗?
Why CPython call
len(iterable)
? From this question I see you won't know an iterator's length until you iterate throw it. Is this an optimization?
当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
上定义,它被调用并且其 return 值用于为新元组分配内存,因为 malloc
失败,最终引发 MemoryError
错误。
/* Guess result size and allocate space. */
n = PyObject_LengthHint(v, 10);
if (n == -1)
goto Fail;
result = PyTuple_New(n);
Can
__len__
method return the 'fake' length (i.e. not the real number of elements in memory) of an object?
在这种情况下,是的。
当__len__
的return值小于需要时,python会在填充元组时调整新元组对象的内存space以适应。如果它比需要的大,虽然 python 会分配额外的内存,但最后会调用 _PyTuple_Resize
来回收过度分配的 space.