首页 > Python > 为什么”1000000000000000 in range(1000000000000001)” 如此的快在Python 3?

为什么”1000000000000000 in range(1000000000000001)” 如此的快在Python 3?

上一篇 下一篇

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

在这种情况下,我本以为以下行会花费过多的时间,因为为了确定 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 实现的函数优化潜在不一致的信息/警告。abarnert 的另一个答案更详细,并为那些对 Python 3 优化背后的历史(以及 Python 2 中缺乏优化)感兴趣的人提供了链接。poke和wim的答案为感兴趣的人提供了相关的C源代码和解释。range__contains__xrange

分割线

网友回答:

这里的根本误解在于认为这是一个生成器。不是。事实上,它不是任何类型的迭代器。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

a 和 a 之间的区别在于 a 是惰性动态序列;它不会记住它的所有值,它只记住它的 、 和 ,并在 上按需创建值。rangelistrangestartstopstep__getitem__

(作为旁注,如果您 ,您会注意到它使用与 相同的类型。这是怎么回事?A 没有使用任何特殊的东西,除了它提供了 的 C 实现,所以它也工作正常。print(iter(a))rangelistiteratorlistlistiteratorlist__getitem__range


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

但是语言中似乎没有任何内容可以保证这种情况会发生。正如Ashwini Chaudhari指出的那样,如果你给它一个非整数值,而不是转换为整数并进行数学测试,它将回退到迭代所有值并逐个比较它们。仅仅因为CPython 3.2+和PyPy 3.x版本恰好包含这种优化,而且这是一个明显的好主意并且很容易做到,IronPython或NewKickAssPython 3.x没有理由不能忽略它。(事实上,CPython 3.0-3.1 不包括它。


如果实际上是一个生成器,比如,那么以这种方式进行测试是没有意义的,或者至少它有意义的方式不会很明显。如果您已经迭代了前 3 个值,生成器仍然是吗?是否应该进行测试导致它迭代并使用所有值,直到(或最多第一个值)?rangemy_crappy_range__contains__1in11>= 1

分割线

网友回答:

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

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

从对象文档中:range()

与常规 OR 相比,该类型的优点是 range 对象将始终占用相同(少量)的内存,无论它表示的范围的大小如何(因为它只存储 和值,根据需要计算单个项目和子范围)。rangelisttuplestartstopstep

因此,您的对象至少可以执行以下操作: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

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

我还简化了实现,只关注整数测试;如果为实际对象指定一个非整数值(包括 的子类),则会启动慢速扫描以查看是否存在匹配项,就像对所有包含值的列表使用包含测试一样。这样做是为了继续支持其他数值类型,这些类型恰好支持整数的相等性测试,但预计不支持整数算术。请参阅实现包含测试的原始 Python 问题。__contains__range()int


* 接近常量时间,因为 Python 整数是无界的,因此数学运算也会随着 N 的增长而增长,使其成为 O(log N) 运算。由于它都是在优化的 C 代码中执行的,并且 Python 将整数值存储在 30 位块中,因此在看到由于此处涉及的整数大小而导致的任何性能影响之前,内存会耗尽。

分割线

网友回答:

使用源头,卢克!

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

  1. 检查数字是否介于 和 和 之间startstop
  2. 检查步幅值是否不会“跨过”我们的数字。

例如,在 中是因为:994range(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);
}

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

/* positive steps: start <= ob < stop */
/* negative steps: stop < ob <= start */
/* result = ((int(ob) - start) % step) == 0 */ 

最后要注意的是 – 查看代码片段底部的函数。如果确切的类型检查失败,那么我们不使用所描述的聪明算法,而是回退到使用 !您可以在解释器中检查此行为(我在这里使用 v3.5.0):range_contains_PySequence_IterSearch

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

模板简介:该模板名称为【为什么”1000000000000000 in range(1000000000000001)” 如此的快在Python 3?】,大小是暂无信息,文档格式为.编程语言,推荐使用Sublime/Dreamweaver/HBuilder打开,作品中的图片,文字等数据均可修改,图片请在作品中选中图片替换即可,文字修改直接点击文字修改即可,您也可以新增或修改作品中的内容,该模板来自用户分享,如有侵权行为请联系网站客服处理。欢迎来懒人模板【Python】栏目查找您需要的精美模板。

相关搜索
  • 下载密码 lanrenmb
  • 下载次数 223次
  • 使用软件 Sublime/Dreamweaver/HBuilder
  • 文件格式 编程语言
  • 文件大小 暂无信息
  • 上传时间 02-13
  • 作者 网友投稿
  • 肖像权 人物画像及字体仅供参考
栏目分类 更多 >
热门推荐 更多 >
微信素材 响应式 微信图片 微信文章 微信公众平台 单页式简历模板 微信模板 自适应 企业网站 html5
您可能会喜欢的其他模板