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

时间:2015-05-06 15:32:43

标签: python performance python-3.x range python-internals

据我了解,range()函数(实际上是an object type in 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' answer是为了完整性,但也请abarnert's first answer详细讨论range成为完整的序列的含义在Python 3中,以及关于Python实现中__contains__函数优化的潜在不一致性的一些信息/警告。 abarnert's other answer详细介绍了一些内容,并为那些对Python 3中优化背后的历史感兴趣的人提供了链接(并且缺少Python 2中xrange的优化)。答案by pokeby wim为感兴趣的人提供相关的C源代码和解释。

12 个答案:

答案 0 :(得分:1686)

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

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

来自range() object documentation

  

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 issue

答案 1 :(得分:679)

这里的根本误解是认为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 lazy 动态序列;它不会记住它的所有值,它只会记住它的startstopstep,并在__getitem__上按需创建值。

(作为旁注,如果您print(iter(a)),您会注意到range使用与listiterator相同的list类型。这是如何工作的? listiterator并未对list使用任何特殊内容,除非它提供__getitem__的C实现,因此它也适用于range。)

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

保证这种情况似乎不会出现在语言中的任何内容。正如Ashwini Chaudhari指出的那样,如果给它一个非整数值,而不是转换为整数并进行数学测试,它将回退到迭代所有值并逐个比较它们。而且仅仅因为CPython 3.2+和PyPy 3.x版本恰好包含了这种优化,而且这是一个明显的好主意并且很容易做到,因此没有理由认为IronPython或NewKickAssPython 3.x无法实现这一目标。不要把它留下来。 (事实上​​CPython 3.0-3.1 没有包含它。)

如果range实际上是一个生成器,例如my_crappy_range,那么以这种方式测试__contains__是不合理的,或者至少它是否有意义#n}&# 39;显而易见。如果您已经迭代了前3个值,1仍然是in生成器吗?如果对1进行测试会导致其迭代并使用最多1的所有值(或最高值>= 1)?

答案 2 :(得分:326)

使用source,Luke!

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

  1. 检查号码是否在startstop之间,以及
  2. 检查步幅值是否超过&#34;我们的号码。
  3. 例如,994位于range(4, 1000, 2),因为:

    1. 4 <= 994 < 1000
    2. (994 - 4) % 2 == 0
    3. 完整的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);
      }
      

      &#34;肉&#34; the line中提到了这个想法:

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

答案 3 :(得分:118)

要添加到Martijn的答案,这是the source的相关部分(在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

答案 4 :(得分:88)

如果您想知道为什么此优化已添加到range.__contains__,以及为什么添加到2.7中的xrange.__contains__

首先,正如Ashwini Chaudhary所发现的,issue 1766304被明确打开以优化[x]range.__contains__。这个补丁是accepted and checked in for 3.2,但没有向后移植到2.7,因为“xrange在很长一段时间内表现得像这样,我不知道它为什么买这个补丁这么晚了。” (2.7当时差不多了。)

同时

最初,xrange是一个不完全序列的对象。正如the 3.1 docs所说:

  

Range对象的行为非常少:它们只支持索引,迭代和len函数。

这不是真的; xrange对象实际上支持自动编制索引的其他一些内容以及len * ,包括__contains__(通过线性搜索)。但当时没有人认为值得制作完整的序列。

然后,作为实施Abstract Base Classes PEP的一部分,重要的是要弄清楚应该将哪些内置类型标记为实现哪些ABC,xrange / range声称要实现{ {1}},即使它仍然只处理相同的“非常小的行为”。在issue 9213之前,没有人注意到这个问题。针对该问题的补丁不仅将collections.Sequenceindex添加到3.2 count,还重新优化了range(与{__contains__共享相同的数学运算1}},并由index直接使用。 ** This change也进入3.2,并没有向后移植到2.x,因为“它是一个添加新方法的bugfix“。 (此时,2.7已经超过了rc状态。)

因此,有两次机会将这种优化反向移植到2.7,但它们都被拒绝了。

*实际上,您甚至可以通过count和索引免费获得迭代,但in 2.3 len个对象获得了自定义迭代器。然后它们在3.x中丢失,它使用与xrange相同的listiterator类型。

**第一个版本实际上重新实现了它,并且错误地得到了详细信息 - 例如,它会给你list。但Daniel Stutzbach的修补程序更新版本恢复了以前的大多数代码,包括在优化不适用时隐式使用3.2 MyIntSubclass(2) in range(5) == False之前的泛型_PySequence_IterSearch的回退。

答案 5 :(得分:39)

其他答案已经很好地解释了,但我想提供另一个实验来说明范围对象的性质:

>>> r = range(5)
>>> for i in r:
        print(i, 2 in r, list(r))

0 True [0, 1, 2, 3, 4]
1 True [0, 1, 2, 3, 4]
2 True [0, 1, 2, 3, 4]
3 True [0, 1, 2, 3, 4]
4 True [0, 1, 2, 3, 4]

正如您所看到的,范围对象是一个记住其范围的对象,可以多次使用(即使在迭代时也是如此),而不仅仅是一次性生成器。

答案 6 :(得分:13)

所有关于评估的懒惰方法和range的一些额外优化。 范围内的值不需要计算直到实际使用,或者甚至由于额外的优化而进一步计算。

顺便说一句,你的整数不是很大,考虑sys.maxsize

sys.maxsize in range(sys.maxsize) 非常快

由于优化 - 很容易比较给定的整数,只有最小和最大范围。

但:

float(sys.maxsize) in range(sys.maxsize) 非常慢

(在这种情况下,range没有优化,所以如果python收到意外的浮点数,python会比较所有数字)

您应该了解实施细节,但不应该依赖,因为将来可能会改变。

答案 7 :(得分:7)

C#中的

Here is实现。您可以看到Contains在O(1)时间内的工作方式。

public struct Range
{
    private readonly int _start;
    private readonly int _stop;
    private readonly int _step;

    // other members omitted for brevity

    public bool Contains(int number)
    {
        // precheck - if the number is not in a valid step point, return false
        // for example, if start=5, step=10, stop=1000, it is obvious that 163 is not in this range (due to remainder)

        if ((_start % _step + _step) % _step != (number % _step + _step) % _step)
            return false;

        // with the help of step sign, we can check borders in linear manner
        int s = Math.Sign(_step);

        // no need if/else to handle both cases - negative and positive step    
        return number * s >= _start * s && number * s < _stop * s;
    }
}

答案 8 :(得分:4)

TL; DR

range()返回的对象实际上是一个range对象。该对象实现了迭代器接口,因此您可以像生成器一样顺序地迭代其值,但是它也实现了__contains__接口,当对象出现在{{的右侧时, 1}}运算符。 in方法返回布尔值,表明该项目是否在对象中。由于__contains__()对象知道其边界和步幅,因此在O(1)中非常容易实现。

答案 9 :(得分:1)

为大的x-1 in (i for i in range(x))尝试x,它使用生成器理解来避免调用range.__contains__优化。

答案 10 :(得分:0)

  1. 由于优化,将给定的整数与最小和最大范围进行比较非常容易。
  2. range()函数之所以在Python3中如此之快的原因是,这里我们对边界使用数学推理,而不是范围对象的直接迭代。
  3. 因此在这里解释逻辑:
    • 检查数字是否在开始和结束之间。
    • 检查步数精度值是否不超过我们的数字。
  4. 举个例子, 997在范围(4,1000,3)中,因为:

    4 <= 997 < 1000, and (997 - 4) % 3 == 0.

答案 11 :(得分:0)

TLDR; range是一个算术级数,因此它可以很容易地计算出对象是否存在。如果以非常快的速度列出列表,它甚至可以获得索引。