据我所知,range()函数实际上是Python 3中的一种对象类型,它动态生成其内容,类似于生成器。

在这种情况下,我预计下一行将花费大量时间,因为为了确定1万亿是否在该范围内,必须生成1万亿值:

1_000_000_000_000_000 in range(1_000_000_000_000_001)

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

我也尝试过类似的方法,但计算仍然几乎是即时的:

# count by tens
1_000_000_000_000_000_000_000 in range(0,1_000_000_000_000_000_000_001,10)

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

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

range()对象在引擎盖下做什么使其如此快速?


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


当前回答

TL;博士

range()返回的对象实际上是一个range对象。此对象实现迭代器接口,因此您可以像生成器、列表或元组一样顺序地迭代其值。

但它也实现了__contains__接口,当对象出现在in运算符的右侧时,实际上会调用该接口。__contains__()方法返回一个bool,表示in左侧的项是否在对象中。因为范围对象知道它们的边界和步幅,所以这很容易在O(1)中实现。

其他回答

这里的根本误解是认为射程是一个发生器。事实并非如此。事实上,它不是任何类型的迭代器。

你可以很容易地看出这一点:

>>> 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))
[]

实际的范围是一个序列,就像一个列表。你甚至可以测试一下:

>>> 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

范围和列表的区别在于,范围是一个惰性或动态序列;它不记得所有的值,只记得开始、停止和步骤,并根据需要在getitem上创建值。

(顺便说一句,如果您打印(iter(a)),您会注意到range使用与list相同的listiterator类型。这是如何工作的?listiterator除了提供了__getitem_的C实现之外,并没有使用任何关于list的特殊功能,所以它也适用于range。)


现在,没有什么能说明序列__事实上,contains必须是常数时间,对于像list这样的序列的明显例子,它不是。但没有什么能说明这是不可能的。而且实现射程更容易__包含只是在数学上检查它((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是否仍在生成器中?对1的测试是否会导致它迭代并消耗所有值,直到1(或直到第一个值>=1)?

对于较大的x值,请尝试x-1 in(i代表i in range(x)),这使用生成器理解来避免调用范围__包含优化。

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

该对象还实现了该对象__contains_hook,并计算您的数字是否属于其范围。计算是一个(接近)恒定的时间操作*。永远不需要扫描范围内所有可能的整数。

从range()对象文档中:

与常规列表或元组相比,范围类型的优势在于,范围对象将始终占用相同(少量)的内存,无论其所代表的范围大小(因为它只存储开始、停止和步长值,根据需要计算单个项和子范围)。

因此,range()对象至少可以做到:

class my_range:
    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, step = stop, start, -step
        else:
            lo, hi = start, stop
        self.length = 0 if lo > hi else ((hi - lo - 1) // 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('my_range object index out of range')

    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

这仍然缺少real-range()支持的一些东西(例如.index()或.count()方法、哈希、相等测试或切片),但应该会给你一个想法。

我还简化了__contains__实现,只关注整数测试;如果给一个real-range()对象一个非整数值(包括int的子类),就会启动一个慢扫描,看看是否匹配,就像对所有包含值的列表使用包含测试一样。这样做是为了继续支持其他恰好支持整数相等测试但不支持整数算术的数字类型。请参阅实现包含测试的Python原始问题。


*接近常数时间,因为Python整数是无限的,所以数学运算也随着N的增长而随时间增长,因此这是一个O(logN)运算。由于它都是在经过优化的C代码中执行的,并且Python将整数值存储在30位块中,因此在您看到由于此处涉及的整数的大小而产生的任何性能影响之前,您可能会耗尽内存。

太长,读不下去了范围是一个算术级数,因此它可以非常容易地计算对象是否存在。它甚至可以得到它的索引,如果它真的像列表一样快速。

使用来源,卢克!

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

检查数字是否介于开始和停止之间,以及检查步幅值是否“超过”我们的数字。

例如,994在范围(41000,2)内,因为:

4<=994<1000,以及(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);
}

评论行中提到了这个想法的“肉”:

/* positive steps: start <= ob < stop */
/* negative steps: stop < ob <= start */
/* 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)