【问题标题】:Why CPython call len(iterable) when executing func(*iterable)?为什么 CPython 在执行 func(*iterable) 时会调用 len(iterable)?
【发布时间】: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

所以我的问题是:

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

  2. __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)——序列分配太大。对吗?

标签: python cpython


【解决方案1】:

为什么 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来回收过度分配的空间。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-07
    • 2012-01-04
    • 2013-12-25
    • 2014-04-29
    • 2017-08-25
    • 1970-01-01
    • 2012-01-18
    • 2016-06-16
    相关资源
    最近更新 更多