为什么 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

所以我的问题是:

  1. 为什么CPython调用len(iterable)?从 this question 我看到在迭代 throw 之前你不会知道迭代器的长度。这是优化吗?

  2. 可以__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.