为什么“1000000000000000 in range(1000000000000001)”在 Python 3 中这么快?
Why is "1000000000000000 in range(1000000000000001)" so fast in Python 3?
据我了解,range()
函数实际上是 an object type in Python 3,它会动态生成其内容,类似于生成器。
在这种情况下,我预计以下行会花费过多的时间,因为为了确定 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()
对象在幕后做了什么让它如此之快?
was chosen for its completeness, but also see for a good discussion of what it means for range
to be a full-fledged sequence in Python 3, and some information/warning regarding potential inconsistency for __contains__
function optimization across Python implementations. goes into some more detail and provides links for those interested in the history behind the optimization in Python 3 (and lack of optimization of xrange
in Python 2). Answers and 提供相关的C源码和解释,有兴趣的可以参考
Python 3 range()
对象不会立即产生数字;它是一个智能 sequence object,可以根据需要 生成数字。它只包含您的开始值、停止值和步长值,然后当您遍历该对象时,每次迭代都会计算下一个整数。
该对象还实现了 object.__contains__
hook,如果您的号码在其范围内,则 计算 。计算是一个(接近)常数时间操作 *。永远不需要扫描范围内所有可能的整数。
来自range()
object documentation:
The advantage of the range
type over a regular list
or tuple
is that a range object will always take the same (small) amount of memory, no matter the size of the range it represents (as it only stores the start
, stop
and step
values, calculating individual items and subranges as needed).
因此,您的 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
这仍然缺少真正的 range()
支持的一些东西(例如 .index()
或 .count()
方法、散列、相等性测试或切片),但应该给你一个想法。
我还简化了 __contains__
实现,只关注整数测试;如果你给一个真正的 range()
对象一个非整数值(包括 int
的子类),就会启动一个慢速扫描来查看是否匹配,就像你对一个对象使用包含测试一样所有包含值的列表。这样做是为了继续支持其他数字类型,这些数字类型恰好支持整数的相等性测试,但预计也不会支持整数算术。请参阅实施遏制测试的原始 Python issue。
* Near 常数时间,因为 Python 整数是无界的,因此数学运算也会随着 N 的增长而增长,从而使其成为 O(log N) 运算。由于它全部在优化的 C 代码中执行,并且 Python 将整数值存储在 30 位块中,因此在您看到由于此处涉及的整数大小而对性能产生任何影响之前,您会 运行 内存不足。
为了补充 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
对象(也就是Python3中的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
使用source,卢克!
在 CPython 中,range(...).__contains__
(一个方法包装器)最终将委托给一个简单的计算来检查值是否可能在范围内。这里速度的原因是我们使用 关于边界的数学推理,而不是范围对象的直接迭代 。解释一下使用的逻辑:
- 检查数字是否在
start
和stop
之间,以及
- 检查步幅值是否“超过”我们的数字。
例如,994
在 range(4, 1000, 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);
}
中提到了这个想法的“内容”
/* 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)
这里的根本误解是认为 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
range
和 list
的区别在于 range
是 lazy 或 dynamic序列;它不会记住它的所有值,它只记住它的 start
、stop
和 step
,并根据需要在 __getitem__
.[=43= 上创建值]
(作为旁注,如果您 print(iter(a))
,您会注意到 range
使用与 list
相同的 listiterator
类型。这是如何工作的? listiterator
除了提供 __getitem__
的 C 实现之外,没有使用任何关于 list
的特殊之处,因此它也适用于 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
)?
其他答案已经很好地解释了,但我想提供另一个实验来说明范围对象的性质:
>>> 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]
如您所见,range
对象是一个可以记住其范围并可以多次使用(即使在迭代它时)的对象,而不仅仅是一次性生成器。
如果您想知道为什么将此优化添加到 range.__contains__
,以及为什么 没有 添加到xrange.__contains__
在 2.7 中:
首先,正如 Ashwini Chaudhary 发现的那样,issue 1766304 was opened explicitly to optimize [x]range.__contains__
. A patch for this was accepted and checked in for 3.2,但没有向后移植到 2.7,因为“xrange
已经这样运行了很长时间,我看不出它能给我们带来什么这么晚才提交补丁。” (那时 2.7 快要出来了。)
同时:
最初,xrange
是一个不完全序列对象。正如 the 3.1 docs 所说:
Range objects have very little behavior: they only support indexing, iteration, and the len
function.
这不是真的; xrange
对象实际上支持一些其他的东西,这些东西会随着索引和 len
、* 自动出现,包括 __contains__
(通过线性搜索)。但当时没有人认为值得将它们制作成完整序列。
然后,作为实施的一部分,Abstract Base Classes PEP, it was important to figure out which builtin types should be marked as implementing which ABCs, and xrange
/range
claimed to implement collections.Sequence
, even though it still only handled the same "very little behavior". Nobody noticed that problem until issue 9213. The patch for that issue not only added index
and count
to 3.2's range
, it also re-worked the optimized __contains__
(which shares the same math with index
, and is directly used by count
).** This change 也进入了 3.2,并且没有向后移植到 2.x,因为“这是一个添加新方法的错误修复”。 (此时,2.7 已经过 rc 状态。)
因此,有两次机会将此优化反向移植到 2.7,但都被拒绝了。
* 事实上,您甚至可以通过单独的索引免费获得迭代,但是 in 2.3 xrange
对象有一个自定义迭代器。
** 第一个版本实际上重新实现了它,并在细节上弄错了——例如,它会给你 MyIntSubclass(2) in range(5) == False
。但是 Daniel Stutzbach 的更新版本的补丁恢复了大部分以前的代码,包括回退到通用的、慢速的 _PySequence_IterSearch
,pre-3.2 range.__contains__
在优化不适用时隐式使用。
这完全是关于 惰性方法 的评估和一些 额外优化 range
。
范围内的值在实际使用之前不需要计算,或者由于额外的优化而进一步计算。
顺便说一句,你的整数不是那么大,考虑sys.maxsize
sys.maxsize in range(sys.maxsize)
相当快
由于优化 - 很容易将给定的整数与范围的最小值和最大值进行比较。
但是:
Decimal(sys.maxsize) in range(sys.maxsize)
相当慢。
(在这种情况下,range
中没有优化,因此如果python收到意外的Decimal,python将比较所有数字)
您应该了解实施细节,但不应依赖于此,因为将来可能会发生变化。
TL;DR
range()
返回的对象实际上是一个range
对象。该对象实现了迭代器接口,因此您可以按顺序迭代其值,就像生成器、列表或元组一样。
但是它也实现了__contains__
接口,当一个对象出现在in
运算符的右侧时,它实际上是被调用的接口. __contains__()
方法 returns bool
判断 in
左边的项目是否在对象中。由于 range
对象知道它们的边界和步幅,这很容易在 O(1) 中实现。
- 由于优化,比较给定整数与最小和最大范围非常容易。
- range()函数之所以在Python3中这么快是因为这里我们对边界使用了数学推理,而不是直接迭代范围对象。
- 所以为了解释这里的逻辑:
- 检查号码是否在start和stop之间
- 检查步长精度值是否不超过我们的数字。
举个例子,997 在范围(4, 1000, 3) 因为:
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
尝试 x-1 in (i for i in range(x))
以获得较大的 x
值,它使用生成器理解来避免调用 range.__contains__
优化。
TLDR;
range
是一个算术级数,所以它可以很容易地计算出对象是否存在。如果它像列表一样非常快,它甚至可以获得它的索引。
据我了解,range()
函数实际上是 an object type in Python 3,它会动态生成其内容,类似于生成器。
在这种情况下,我预计以下行会花费过多的时间,因为为了确定 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()
对象在幕后做了什么让它如此之快?
range
to be a full-fledged sequence in Python 3, and some information/warning regarding potential inconsistency for __contains__
function optimization across Python implementations. xrange
in Python 2). Answers
Python 3 range()
对象不会立即产生数字;它是一个智能 sequence object,可以根据需要 生成数字。它只包含您的开始值、停止值和步长值,然后当您遍历该对象时,每次迭代都会计算下一个整数。
该对象还实现了 object.__contains__
hook,如果您的号码在其范围内,则 计算 。计算是一个(接近)常数时间操作 *。永远不需要扫描范围内所有可能的整数。
来自range()
object documentation:
The advantage of the
range
type over a regularlist
ortuple
is that a range object will always take the same (small) amount of memory, no matter the size of the range it represents (as it only stores thestart
,stop
andstep
values, calculating individual items and subranges as needed).
因此,您的 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
这仍然缺少真正的 range()
支持的一些东西(例如 .index()
或 .count()
方法、散列、相等性测试或切片),但应该给你一个想法。
我还简化了 __contains__
实现,只关注整数测试;如果你给一个真正的 range()
对象一个非整数值(包括 int
的子类),就会启动一个慢速扫描来查看是否匹配,就像你对一个对象使用包含测试一样所有包含值的列表。这样做是为了继续支持其他数字类型,这些数字类型恰好支持整数的相等性测试,但预计也不会支持整数算术。请参阅实施遏制测试的原始 Python issue。
* Near 常数时间,因为 Python 整数是无界的,因此数学运算也会随着 N 的增长而增长,从而使其成为 O(log N) 运算。由于它全部在优化的 C 代码中执行,并且 Python 将整数值存储在 30 位块中,因此在您看到由于此处涉及的整数大小而对性能产生任何影响之前,您会 运行 内存不足。
为了补充 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
对象(也就是Python3中的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
使用source,卢克!
在 CPython 中,range(...).__contains__
(一个方法包装器)最终将委托给一个简单的计算来检查值是否可能在范围内。这里速度的原因是我们使用 关于边界的数学推理,而不是范围对象的直接迭代 。解释一下使用的逻辑:
- 检查数字是否在
start
和stop
之间,以及 - 检查步幅值是否“超过”我们的数字。
例如,994
在 range(4, 1000, 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);
}
中提到了这个想法的“内容”
/* 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)
这里的根本误解是认为 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
range
和 list
的区别在于 range
是 lazy 或 dynamic序列;它不会记住它的所有值,它只记住它的 start
、stop
和 step
,并根据需要在 __getitem__
.[=43= 上创建值]
(作为旁注,如果您 print(iter(a))
,您会注意到 range
使用与 list
相同的 listiterator
类型。这是如何工作的? listiterator
除了提供 __getitem__
的 C 实现之外,没有使用任何关于 list
的特殊之处,因此它也适用于 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
)?
其他答案已经很好地解释了,但我想提供另一个实验来说明范围对象的性质:
>>> 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]
如您所见,range
对象是一个可以记住其范围并可以多次使用(即使在迭代它时)的对象,而不仅仅是一次性生成器。
如果您想知道为什么将此优化添加到 range.__contains__
,以及为什么 没有 添加到xrange.__contains__
在 2.7 中:
首先,正如 Ashwini Chaudhary 发现的那样,issue 1766304 was opened explicitly to optimize [x]range.__contains__
. A patch for this was accepted and checked in for 3.2,但没有向后移植到 2.7,因为“xrange
已经这样运行了很长时间,我看不出它能给我们带来什么这么晚才提交补丁。” (那时 2.7 快要出来了。)
同时:
最初,xrange
是一个不完全序列对象。正如 the 3.1 docs 所说:
Range objects have very little behavior: they only support indexing, iteration, and the
len
function.
这不是真的; xrange
对象实际上支持一些其他的东西,这些东西会随着索引和 len
、* 自动出现,包括 __contains__
(通过线性搜索)。但当时没有人认为值得将它们制作成完整序列。
然后,作为实施的一部分,Abstract Base Classes PEP, it was important to figure out which builtin types should be marked as implementing which ABCs, and xrange
/range
claimed to implement collections.Sequence
, even though it still only handled the same "very little behavior". Nobody noticed that problem until issue 9213. The patch for that issue not only added index
and count
to 3.2's range
, it also re-worked the optimized __contains__
(which shares the same math with index
, and is directly used by count
).** This change 也进入了 3.2,并且没有向后移植到 2.x,因为“这是一个添加新方法的错误修复”。 (此时,2.7 已经过 rc 状态。)
因此,有两次机会将此优化反向移植到 2.7,但都被拒绝了。
* 事实上,您甚至可以通过单独的索引免费获得迭代,但是 in 2.3 xrange
对象有一个自定义迭代器。
** 第一个版本实际上重新实现了它,并在细节上弄错了——例如,它会给你 MyIntSubclass(2) in range(5) == False
。但是 Daniel Stutzbach 的更新版本的补丁恢复了大部分以前的代码,包括回退到通用的、慢速的 _PySequence_IterSearch
,pre-3.2 range.__contains__
在优化不适用时隐式使用。
这完全是关于 惰性方法 的评估和一些 额外优化 range
。
范围内的值在实际使用之前不需要计算,或者由于额外的优化而进一步计算。
顺便说一句,你的整数不是那么大,考虑sys.maxsize
sys.maxsize in range(sys.maxsize)
相当快
由于优化 - 很容易将给定的整数与范围的最小值和最大值进行比较。
但是:
Decimal(sys.maxsize) in range(sys.maxsize)
相当慢。
(在这种情况下,range
中没有优化,因此如果python收到意外的Decimal,python将比较所有数字)
您应该了解实施细节,但不应依赖于此,因为将来可能会发生变化。
TL;DR
range()
返回的对象实际上是一个range
对象。该对象实现了迭代器接口,因此您可以按顺序迭代其值,就像生成器、列表或元组一样。
但是它也实现了__contains__
接口,当一个对象出现在in
运算符的右侧时,它实际上是被调用的接口. __contains__()
方法 returns bool
判断 in
左边的项目是否在对象中。由于 range
对象知道它们的边界和步幅,这很容易在 O(1) 中实现。
- 由于优化,比较给定整数与最小和最大范围非常容易。
- range()函数之所以在Python3中这么快是因为这里我们对边界使用了数学推理,而不是直接迭代范围对象。
- 所以为了解释这里的逻辑:
- 检查号码是否在start和stop之间
- 检查步长精度值是否不超过我们的数字。
举个例子,997 在范围(4, 1000, 3) 因为:
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
尝试 x-1 in (i for i in range(x))
以获得较大的 x
值,它使用生成器理解来避免调用 range.__contains__
优化。
TLDR;
range
是一个算术级数,所以它可以很容易地计算出对象是否存在。如果它像列表一样非常快,它甚至可以获得它的索引。