为什么CPython在执行func(* iterable)时会调用len(iterable)?

时间:2017-11-01 05:05:55

标签: python cpython

最近我正在编写一个下载程序,它使用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

所以我的问题是:

  1. 为什么CPython会调用len(iterable)?从this question开始,我看到你不会知道迭代器的长度,直到你迭代抛出它。这是优化吗?

  2. __len__方法可以返回对象的'假'长度(即不是内存中元素的实际数量)吗?

1 个答案:

答案 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来回收过度分配的空间。