据我所知,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源代码和解释。


当前回答

为了补充Martijn的答案,这是源代码的相关部分(在C中,因为范围对象是用本机代码编写的):

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);
}

因此,对于PyLong对象(在Python 3中为int),它将使用range_contains_long函数来确定结果。该函数本质上检查ob是否在指定范围内(尽管在C中看起来有点复杂)。

如果它不是int对象,则返回到迭代,直到找到值(或没有)。

整个逻辑可以转换为伪Python,如下所示:

def range_contains (rangeObj, obj):
    if isinstance(obj, int):
        return range_contains_long(rangeObj, obj)

    # default logic by iterating
    return any(obj == x for x in rangeObj)

def range_contains_long (r, num):
    if r.step > 0:
        # positive step: r.start <= num < r.stop
        cmp2 = r.start <= num
        cmp3 = num < r.stop
    else:
        # negative step: r.start >= num > r.stop
        cmp2 = num <= r.start
        cmp3 = r.stop < num

    # outside of the range boundaries
    if not cmp2 or not cmp3:
        return False

    # num must be on a valid step inside the boundaries
    return (num - r.start) % r.step == 0

其他回答

如果您想知道为什么要将此优化添加到范围中__contains_,以及为什么没有将其添加到xrange中__包含2.7中的_:

首先,正如Ashwini Chaudhary所发现的,1766304期被明确地开放,以优化[x]范围__包含_。这个补丁已经被接受并以3.2的价格签入,但没有被移植到2.7,因为“xrange已经这么长时间了,我看不出它能让我们这么晚才提交补丁。”

同时:

最初,xrange是一个不太序列的对象。正如3.1文档所说:

范围对象的行为很少:它们只支持索引、迭代和len函数。

这并不完全正确;一个xrange对象实际上还支持一些其他的东西,这些东西是通过索引和len自动实现的,包括contains(通过线性搜索)。但当时没有人认为制作完整的序列是值得的。

然后,作为实现抽象基类PEP的一部分,重要的是弄清楚哪些内置类型应该标记为实现哪些ABC,以及xrange/range声称实现集合.Sequence,尽管它仍然只处理相同的“非常少的行为”。直到第9213期,没有人注意到这个问题。针对该问题的补丁不仅将索引和计数添加到了3.2的范围,还重新使用了优化的__contains__(它与索引共享相同的数学,并且直接由计数使用)。**这一更改也适用于3.2,并且没有后移到2.x,因为“这是一个添加新方法的错误修复程序”。(此时,2.7已经超过了rc状态。)

因此,有两次机会将优化后移到2.7,但都被拒绝了。


*事实上,您甚至可以仅通过索引免费获得迭代,但在2.3xrange中,对象有一个自定义迭代器。

**第一个版本实际上重新实现了它,并错误地获得了详细信息,例如,它将在范围(5)==False中为您提供MyIntSubclass(2)。但Daniel Stutzbach的补丁更新版本恢复了以前的大部分代码,包括回退到3.2之前的通用、缓慢的_PySequence_IterSearch__当优化不适用时,contains被隐式使用。

TL;博士

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

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

__contains_方法直接与范围的开始和结束进行比较

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位块中,因此在您看到由于此处涉及的整数的大小而产生的任何性能影响之前,您可能会耗尽内存。

为了补充Martijn的答案,这是源代码的相关部分(在C中,因为范围对象是用本机代码编写的):

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);
}

因此,对于PyLong对象(在Python 3中为int),它将使用range_contains_long函数来确定结果。该函数本质上检查ob是否在指定范围内(尽管在C中看起来有点复杂)。

如果它不是int对象,则返回到迭代,直到找到值(或没有)。

整个逻辑可以转换为伪Python,如下所示:

def range_contains (rangeObj, obj):
    if isinstance(obj, int):
        return range_contains_long(rangeObj, obj)

    # default logic by iterating
    return any(obj == x for x in rangeObj)

def range_contains_long (r, num):
    if r.step > 0:
        # positive step: r.start <= num < r.stop
        cmp2 = r.start <= num
        cmp3 = num < r.stop
    else:
        # negative step: r.start >= num > r.stop
        cmp2 = num <= r.start
        cmp3 = r.stop < num

    # outside of the range boundaries
    if not cmp2 or not cmp3:
        return False

    # num must be on a valid step inside the boundaries
    return (num - r.start) % r.step == 0