协慌网

登录 贡献 社区

为什么 Python 3 中的 “1000000000000000 在范围内(1000000000000001)” 如此之快?

我的理解是, range()函数实际上是 Python 3 中的一个对象类型 ,它可以动态生成其内容,类似于生成器。

在这种情况下,我原本预计下面的行需要花费过多的时间,因为为了确定 1 千万亿是否在该范围内,必须生成一个千万亿的值:

1000000000000000 in range(1000000000000001)

此外:似乎无论我添加多少个零,计算或多或少都需要相同的时间(基本上是瞬时的)。

我也尝试过这样的事情,但计算仍然几乎是即时的:

1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens

如果我尝试实现自己的范围功能,结果就不那么好了!!

def my_crappy_range(N):
    i = 0
    while i < N:
        yield i
        i += 1
    return

在引擎盖下做的range()对象是什么让它如此之快?


选择 Martijn Pieters 的答案是因为它的完整性,但也看到了abarnert 的第一个答案 ,可以很好地讨论range在 Python 3 中成为一个完整的序列的意义 ,以及关于__contains__函数优化的潜在不一致性的一些信息 / 警告 Python 实现。 abarnert 的另一个答案更详细,并为那些对 Python 3 中优化背后的历史感兴趣的人提供了链接(并且缺乏 Python 2 中xrange的优化)。 pokewim 的答案 感兴趣的人提供了相关的 C 源代码和解释。

答案

Python 3 range()对象不会立即生成数字; 它是一个智能序列对象,可以按需生成数字。它包含的只是你的开始值,停止值和步长值,然后在迭代对象时,每次迭代计算下一个整数。

该对象还实现了object.__contains__ hook ,并计算您的数字是否是其范围的一部分。计算是 O(1)恒定时间操作。永远不需要扫描范围内的所有可能的整数。

来自range()对象文档

所述的优点range类型通过常规listtuple是一个范围对象将始终以相同的内存(小)数量,无论它代表的范围内的大小(因为它仅存储startstopstep值,根据需要计算单个项目和子范围)。

所以至少,你的range()对象会:

class my_range(object):
    def __init__(self, start, stop=None, step=1):
        if stop is None:
            start, stop = 0, start
        self.start, self.stop, self.step = start, stop, step
        if step < 0:
            lo, hi = stop, start
        else:
            lo, hi = start, stop
        self.length = ((hi - lo - 1) // abs(step)) + 1

    def __iter__(self):
        current = self.start
        if self.step < 0:
            while current > self.stop:
                yield current
                current += self.step
        else:
            while current < self.stop:
                yield current
                current += self.step

    def __len__(self):
        return self.length

    def __getitem__(self, i):
        if i < 0:
            i += self.length
        if 0 <= i < self.length:
            return self.start + i * self.step
        raise IndexError('Index out of range: {}'.format(i))

    def __contains__(self, num):
        if self.step < 0:
            if not (self.stop < num <= self.start):
                return False
        else:
            if not (self.start <= num < self.stop):
                return False
        return (num - self.start) % self.step == 0

这仍然缺少一些真正的range()支持的东西(例如.index().count()方法,散列,等式测试或切片),但应该给你一个想法。

我还简化了__contains__实现,只关注整数测试; 如果为实际的range()对象提供非整数值(包括int子类),则启动慢速扫描以查看是否存在匹配,就像对所有包含的值的列表使用包含测试一样。这样做是为了继续支持恰好支持使用整数进行相等性测试的其他数值类型,但也不希望支持整数运算。查看实现包含测试的原始Python 问题

这里的根本误解在于认为range是一个发电机。不是。实际上,它不是任何一种迭代器。

你可以很容易地说出来:

>>> a = range(5)
>>> print(list(a))
[0, 1, 2, 3, 4]
>>> print(list(a))
[0, 1, 2, 3, 4]

如果它是一个生成器,迭代它就会耗尽它:

>>> b = my_crappy_range(5)
>>> print(list(b))
[0, 1, 2, 3, 4]
>>> print(list(b))
[]

什么range实际上是,是一个序列,就像一个列表。你甚至可以测试这个:

>>> import collections.abc
>>> isinstance(a, collections.abc.Sequence)
True

这意味着它必须遵循作为序列的所有规则:

>>> a[3]         # indexable
3
>>> len(a)       # sized
5
>>> 3 in a       # membership
True
>>> reversed(a)  # reversible
<range_iterator at 0x101cd2360>
>>> a.index(3)   # implements 'index'
3
>>> a.count(3)   # implements 'count'
1

rangelist之间的区别在于range惰性动态序列; 它不记得它的所有值,只记得它的startstopstep ,并在__getitem__上按需创建值。

(作为旁注,如果你print(iter(a)) ,你会发现该range使用与list一样的listiterator类型。它是如何工作的? listiterator器不会对list使用任何特殊的东西,除了事实是它提供了__getitem__的 C 实现,所以它也适用于range 。)


现在,没有什么可以说Sequence.__contains__必须是恒定的时间 - 实际上,对于像list这样的序列的明显例子,它不是。但没有什么能说它不可能 。并且更容易实现range.__contains__以数学方式检查( (val - start) % step ,但处理负步骤有一些额外的复杂性)而不是实际生成和测试所有值,所以为什么不应该这样做它更好的方式?

但是语言似乎没有任何东西可以保证这种情况发生。正如 Ashwini Chaudhari 指出的那样,如果给它一个非整数值,而不是转换为整数并进行数学测试,它将回退到迭代所有值并逐个比较它们。而且只是因为 CPython 3.2 + 和 PyPy 3.x 版本碰巧包含了这个优化,而且这是一个明显的好主意并且很容易做到,因此没有理由认为 IronPython 或 NewKickAssPython 3.x 无法将其排除在外。 (实际上 CPython 3.0-3.1 没有包含它。)


如果range实际上是一个生成器,比如my_crappy_range ,那么以这种方式测试__contains__是没有意义的,或者至少它有意义的方式并不明显。如果你已经迭代了前 3 个值,那么1仍然in生成器中吗?如果测试为1会导致它迭代并消耗最多1所有值(或直到第一个值>= 1 )?

使用来源 ,卢克!

在 CPython 中, range(...).__contains__ (一个方法包装器)最终将委托给一个简单的计算,该计算检查该值是否可能在该范围内。这里速度的原因是我们使用关于边界的数学推理,而不是范围对象的直接迭代 。解释使用的逻辑:

  1. 检查的数量之间的startstop ,并
  2. 检查步幅值是否 “跳过” 我们的号码。

例如, 994range(4, 1000, 2) 994 range(4, 1000, 2)因为:

  1. 4 <= 994 < 1000 ,和
  2. (994 - 4) % 2 == 0

完整的 C 代码包含在下面,由于内存管理和引用计数细节,它有点冗长,但基本思路是:

static int
range_contains_long(rangeobject *r, PyObject *ob)
{
    int cmp1, cmp2, cmp3;
    PyObject *tmp1 = NULL;
    PyObject *tmp2 = NULL;
    PyObject *zero = NULL;
    int result = -1;

    zero = PyLong_FromLong(0);
    if (zero == NULL) /* MemoryError in int(0) */
        goto end;

    /* Check if the value can possibly be in the range. */

    cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
    if (cmp1 == -1)
        goto end;
    if (cmp1 == 1) { /* positive steps: start <= ob < stop */
        cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
        cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
    }
    else { /* negative steps: stop < ob <= start */
        cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
        cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
    }

    if (cmp2 == -1 || cmp3 == -1) /* TypeError */
        goto end;
    if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
        result = 0;
        goto end;
    }

    /* Check that the stride does not invalidate ob's membership. */
    tmp1 = PyNumber_Subtract(ob, r->start);
    if (tmp1 == NULL)
        goto end;
    tmp2 = PyNumber_Remainder(tmp1, r->step);
    if (tmp2 == NULL)
        goto end;
    /* result = ((int(ob) - start) % step) == 0 */
    result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
  end:
    Py_XDECREF(tmp1);
    Py_XDECREF(tmp2);
    Py_XDECREF(zero);
    return result;
}

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

这个想法的 “肉” 在行中提到:

/* result = ((int(ob) - start) % step) == 0 */

最后一点 - 请查看代码段底部的range_contains函数。如果确切的类型检查失败,那么我们不使用所描述的聪明算法,而是使用_PySequence_IterSearch回退到范围的哑迭代搜索!您可以在解释器中检查此行为(我在这里使用 v3.5.0):

>>> x, r = 1000000000000000, range(1000000000000001)
>>> class MyInt(int):
...     pass
... 
>>> x_ = MyInt(x)
>>> x in r  # calculates immediately :) 
True
>>> x_ in r  # iterates for ages.. :( 
^\Quit (core dumped)