据我所知,range()函数实际上是Python3中的对象类型,它可以动态生成其内容,类似于生成器。
在这种情况下,我希望下面一行花费的时间不多,因为为了确定1万亿是否在范围内,必须生成一个万亿值:
1
| 1000000000000000 in range(1000000000000001) |
此外:看起来无论我加多少个零,计算或多或少都需要相同的时间(基本上是瞬时的)。
我也尝试过类似的方法,但计算几乎是即时的:
1
| 1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens |
如果我尝试实现自己的范围函数,结果就不那么好了!!
1 2 3 4 5 6
| def my_crappy_range(N):
i = 0
while i < N:
yield i
i += 1
return |
range()物体在发动机罩下做什么使它如此快速?
Martijn-Pieters的答案之所以被选择是因为它的完整性,但也请参见Abarner的第一个答案,以便对range在python 3中是一个完整的序列意味着什么进行很好的讨论,以及有关在python实现中__contains__函数优化的潜在不一致性的一些信息/警告。Abarnett的另一个答案更为详细,并为那些对python 3优化背后的历史感兴趣的人(以及python 2中缺少xrange的优化)提供了链接。poke和wim的答案为感兴趣的人提供了相关的C源代码和解释。
- 但不要在python 2中使用xrange()。
- 请注意,只有当我们检查的项目是bool或long类型时,这种情况才会发生,而其他对象类型会变得疯狂。试一试:100000000000000.0 in range(1000000000000001)。
- 谁告诉过你range是发电机?
- @我想不是。我把"生成器"和"根据需要生成"混为一谈。我会编辑的。
- @里克蒂奇:不要编辑它,我认为这一点是你困惑的根源。对于一个生成器来说,实现这样的__contains__没有多大意义。
- @我认为我所做的编辑使混乱原封不动。
- 最后一件事:python 3真的保证了这种行为吗?我知道CPython的每一个版本至少有3.1+和Pypy3,从第一个测试版开始提供,但我认为如果Ironpython3.4明天发布并有一个O(N)__contains__方法,它将是完全有效的。
- @Rickthey:事实上,我很肯定我已经知道答案了;当我第一次告诉人们他们可以在python 3(而不是python 2)中使用2 in r时,有人向我提出挑战,要求我在文档中找到它,但它不在那里,当我问到(关于python ideas或bug tracker?我忘记了……)不管它是否应该得到保证,似乎没有人对以这种或那种方式回答问题感兴趣,直到有另一个Python3实现者可以与之交谈。
- @Ashwinichaudhary,python2-xrange不是和python3-range一样吗?
- @最棒的是,这可能是Abarnett在上面得到的东西的一部分?range的实现(大概是xrange的实现)从未在任何地方指定过,因此具体细节(包括__contains__和__item__等)的计算取决于不同的语言实现者?因此,xrange在过去的实施方式可能有所不同。我只是在猜测。
- @里克蒂希:差不多。这仍然提出了一个实际问题,即为什么3.0 range.__contains__优化没有返回到2.6 xrange.__contains__(2.x c api在tp_as_sequence结构中有sq_contains等,所以没有理由不可能是……),但为此,您可能需要跟踪hg的变化,其中优化被添加到py3k中。分支,看看是否有相应的bug和/或线程…
- @superbest xrange()对象没有__contains__方法,因此项目检查必须循环遍历所有项目。另外,range()中的其他变化很少,比如它支持切片(再次返回range对象),现在也有count和index方法使其与collections.Sequenceabc兼容。
- @abarnet没有进行反向移植,因为python 2处于生命支持模式,除了错误修复,没有其他更新。他们考虑给它所有的性能好,但他们说,他们不会在一个政治公众人物(不记得哪个)。这促使我迁移到python 3。
- @阿什维尼查德哈里,为什么不呢?
- 下面是用纯Python实现的python 3 range.__contains__方法
- 总之,python中的range支持通过生成元素从框中签出成员资格,实际上它比成员资格签入列表更快。成员资格签入范围是在恒定时间内完成的。
python 3 range()对象不会立即生成数字;它是一个按需生成数字的智能序列对象。它所包含的只是您的开始、停止和步骤值,然后在对象上迭代时,每次迭代都会计算下一个整数。
对象还实现了object.__contains__钩子,并计算您的数字是否是其范围的一部分。计算是一个O(1)常量时间操作。不需要扫描范围内所有可能的整数。
来自range()对象文档:
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()对象会:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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问题。
- 好吧,例如,计算range(1000000000)[index_number],可能类似于:return start + step * index_number?
- @里克蒂希:是的。如果你能计算出中间数,就不需要产生中间数。
- 我觉得这有点不清楚。range对象不生成元素—它们生成一个生成元素的迭代器。range是一个虚拟序列。
- @Veedrac:我不想让这句话更费劲,更深入地讨论迭代器和Iterables的细节。我来看看我能不能把它重新组装一下。
- 有趣的事实:因为你有一个工作的__getitem__和__len__的实现,所以__iter__的实现实际上是不必要的。
- @Lucretiel:在python 2.3中,特别添加了一个特殊的xrangeiterator,因为速度不够快。然后在3.x中的某个地方(我不确定它是3.0还是3.2),它被扔了,它们使用的是与list相同的listiterator类型。
- @Lucretiel你不需要继承collections.abc.Sequence的遗产吗?
- @Lucretiel嗯,我猜你不会:repl.it/mtp,但在某些情况下可能会更快?
- @尼克:不,仅仅实现序列协议就足够了;参见iter()函数文档。
- @Martijnpieers:这是如何计算的"O(1)常量时间操作"?对于正整数,我看不出任何方法可以比用n除以d的时间更快地计算N in range(0,N+1,D)。
- @两个任意数m和n(任意精度数)的martijnpieers比较应为o(log min(m,n))。只有当M和N的尺寸和精度有硬编码的上限时,O(1)才是真的。
- @stefan.schwetschke:对,在最初编写补丁时,range只支持到sys.maxsize的start和stop值,所以有一个上限。与len(range_object)相比,数字比较/模量接近于足够的常数,不影响此处的分析。
- 我将把构造函数定义为def __init__(self, *start_stop_step),然后从那里解析它;现在标记参数的方式有点混乱。然而,+1;你仍然明确地解释了这种行为。
- @弗雷克斯蒂:看上面的评论,你不是第一个指出这一点的人。:-)您的n整数中有位,而不是范围的长度,甚至我们现在讨论的是每30位python整数值就有一个新的C整数。因此,当聚焦于范围的大小时,计算接近于o(1),而不是所涉及的整数的位大小。
- @很遗憾,这是真类初始值设定项的签名。range比*args旧(比允许c-api函数具有完整python签名的argclinicapi小得多)。其他一些旧函数(以及一些新函数,如xrange、slice和itertools.islice,为了一致性)的工作方式相同,但在大多数情况下,guido和其他核心devs似乎都同意您的观点。2 +文档甚至描述EDCOX1、2和朋友,好像它们是C++风格的重载,而不是显示实际混淆的签名。
- @Codypiersall:事实上,这里引用了guido t he argclinic讨论的一句话,当时Nick Coghlan想出了一种方法来明确定义range:"请不要让人们更容易复制我最糟糕的设计决策。"所以,我很肯定他同意range是一个混乱的书面形式。
- 我很欣赏你对我的采访。感谢您提到这个问题,这是一个有趣的讨论。
- 这里是__getitem__和__contains__纯python实现,它通过了python 3 range()的所有测试。
- 这里有足够多的评论抱怨O(log n)而不是O(1),我认为应该将其编辑到答案中。或"o(1)算术运算"。
- @不,我不同意。这只对值>sys.maxsize起作用,而且由于C实现的固定成本非常低,因此在这种特定情况下,您可以忽略这一点。这种技术细节的讨论在评论中很好,但会分散人们对答案的注意力。
这里的基本误解是认为range是一个生成器。不是这样。实际上,它不是任何类型的迭代器。
你可以很容易地分辨出:
1 2 3 4 5
| >>> a = range(5)
>>> print(list(a))
[0, 1, 2, 3, 4]
>>> print(list(a))
[0, 1, 2, 3, 4] |
如果它是一个生成器,迭代一次就会耗尽它:
1 2 3 4 5
| >>> b = my_crappy_range(5)
>>> print(list(b))
[0, 1, 2, 3, 4]
>>> print(list(b))
[] |
range实际上是一个序列,就像一个列表。您甚至可以测试:
1 2 3
| >>> import collections.abc
>>> isinstance(a, collections.abc.Sequence)
True |
这意味着它必须遵循作为一个序列的所有规则:
1 2 3 4 5 6 7 8 9 10 11 12
| >>> 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是一个懒惰的或动态的序列;它不记得所有的值,只记得它的start、stop和step,并根据需要创建__getitem__的值。
(作为补充说明,如果您使用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的值?
- 这是一件非常重要的事情。我想python 2和3之间的差异可能导致了我在这一点上的困惑。无论如何,我应该认识到,因为range和list一起被列为序列类型。
- @Rickthey:实际上,在2.6+中(我想可能是2.5+),xrange也是一个序列。请参阅2.7文档。事实上,这几乎是一个序列。
- @实际上,我错了;在2.6-2.7(和3.0-3.1)中,它声称是一个序列,但它仍然是一个几乎是序列。看看我的另一个答案。
- 它不是一个迭代器,它是一个序列(在Java方面是可迭代的,C枚举的i枚举)-用一个EDCOX1×10的方法返回一个迭代器。它只能使用一次。
- 似乎奇怪的是,'s' in range(10**10)没有优化到立即返回false。
- @thomasahle:因为range不是整数时不检查类型,因为类型总是可能有一个__eq__与int兼容。当然,str显然不起作用,但他们不想通过明确检查不在其中的所有类型来减慢速度(毕竟,str子类可以覆盖__eq__并包含在range中)。
- 为了简单(以及与python 2的兼容性,因为其中一些点概括为python 2,即使问题本身没有),可以引用collections.Sequence而不是collections.abc.Sequence?
- "与实际生成和测试所有的值相比,实现range.__contains__更容易,只是通过数学方式检查它[………"—我对此表示怀疑,因为您可以简单地省略__contains__的实现来获得后一种行为,这当然比任何需要显式编写的实现都容易。:)
利用源头,卢克!
在cpython中,EDOCX1(方法包装器)最终将委托给一个简单的计算,该计算检查值是否可能在范围内。这里速度的原因是我们使用的是关于边界的数学推理,而不是距离对象的直接迭代。要解释使用的逻辑:
检查数字是否在start和stop之间,以及
检查跨步值是否没有"跨过"我们的数字。
例如,994在range(4, 1000, 2)中,因为:
4 <= 994 < 1000和
(994 - 4) % 2 == 0。
完整的C代码包含在下面,由于内存管理和引用计数详细信息,这有点冗长,但基本思想是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| 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);
} |
这一想法的"肉"在这一行中提到:
1
| /* result = ((int(ob) - start) % step) == 0 */ |
最后一点要注意的是,查看代码段底部的range_contains函数。如果精确的类型检查失败,那么我们不使用所描述的聪明算法,而是使用_PySequence_IterSearch返回到范围的一个愚蠢的迭代搜索!您可以在解释器中检查这种行为(我在这里使用的是v3.5.0):
1 2 3 4 5 6 7 8 9
| >>> 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) |
- 很好的回答。这也是我最喜欢的关于人道使用goto的新例子。
- @Brian_o最好用try: finally:和return来代替goto。
- @wizzwizz4从什么时候开始C支持Try/Finally?(除了某些特定于供应商的扩展…)
要添加到martijn的答案中,这是源代码的相关部分(在C中,因为range对象是用本机代码编写的):
1 2 3 4 5 6 7 8 9
| 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,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 |
- @克里斯韦斯林:我认为这是不同的,足够多的信息(足够多),编辑马蒂的答案在这里是不合适的。这是一个判断的要求,但是人们通常错误的一面是不对别人的答案做出重大的改变。
如果您想知道为什么将此优化添加到range.__contains__中,以及为什么在2.7中不将其添加到xrange.__contains__中:
首先,正如Ashwini Chaudhary发现的那样,1766304号问题被明确地打开,以优化[x]range.__contains__。一个补丁被接受并在3.2版本中签入,但没有返回到2.7版本,因为"xrange的行为如此之久,以至于我看不到它会给我们带来什么,让我们这么晚才提交补丁。"(2.7在那时就快过时了。)
同时:
最初,xrange是一个不完全序列的对象。如3.1文件所述:
Range objects have very little behavior: they only support indexing, iteration, and the len function.
这并不完全正确;一个xrange对象实际上支持一些其他的自动索引和len,包括__contains__(通过线性搜索)。但当时没人认为制作完整的序列是值得的。
然后,作为实现抽象基类PEP的一部分,重要的是要弄清楚哪些内置类型应该标记为实现哪些ABC,而xrange/range声称实现collections.Sequence,尽管它仍然只处理相同的"很少的行为"。在9213版之前,没有人注意到这个问题。该版本的补丁不仅将index和count添加到3.2的range中,还重新运行了优化的__contains__(与index共享相同的数学,并由count直接使用)。**这一变化也应用到了3.2中,并且没有返回到2.x,因为"这是一个添加新方法的错误修复程序"(此时,2.7已经超过了RC状态。)
所以,有两次机会将这个优化返回到2.7,但都被拒绝了。
实际上,您甚至可以使用len和indexing免费获得迭代,但在2.3 xrange对象中获得自定义迭代器。然后在3.x中丢失,它使用与list相同的listiterator类型。
第一个版本实际上重新实现了它,并且得到了错误的细节——例如,它会给你MyIntSubclass(2) in range(5) == False。但是Daniel Stutzbach的补丁更新版本恢复了之前的大部分代码,包括对通用的、缓慢的_PySequence_IterSearch的回退,即3.2版之前的range.__contains__在优化不适用时被隐式使用。
- 从这里的评论来看:改进xrange.__contains__,看起来他们并没有把它移植到python 2,只是为了给用户留下一个惊喜元素,现在已经太晚了。count和index补丁后来被添加了。当时的文件:hg.python.org/c python/file/d59na3f2e72d/objects/rangeobject.&zwnj;&8203;c
- 我有一种阴险的怀疑,一些核心的python开发人员倾向于"强硬的爱"python 2.x,因为他们想鼓励人们转向更高级的python3:。
- @维姆:当然有两个,但我不认为本杰明·彼得森就是这样。他似乎更倾向于"仍在2.7岁的人这样做是因为他们想确保我们不修复那些没有坏掉的东西,所以尽可能少地改变"。但目前最常见的(或至少是有声的)态度似乎是"使从2.x移植到3.x变得容易是优先的1、2和3",这可能是因为多个付费在python上工作的人目前也在付费在Fedora和Ubuntu移植到3.5。
- 我敢打赌,在旧版本中添加新功能是一个巨大的负担。想象一下,如果你去Oracle说,"看,我在Java 1.4上,我应该得到lambda表达式!把它们无偿地装回去。"
- @罗伯特格兰特很好,这个解释的问题是,2.7不老。我相信它和3.3发布的时间差不多。
- @里克蒂奇是的,这只是一个例子。如果我说1.7,它仍然适用。这是数量上的差异,而不是质量上的差异。基本上(无薪的)开发人员不能永远在3.x中制作酷的新东西,并且为那些不想升级的人将其转换为2.x。这是一个巨大而荒谬的负担。你认为我的推理还有问题吗?
- @罗伯特格兰特不,一点也不,你的推理是合理的,我也没有强烈的不同。我只是说它不能完全解释为什么没有为2.7做xrange优化(我理解这不是你说的)。2.7是新版本,而不是旧版本——人们对2.7带来的所有新东西都很兴奋,就像其他新版本一样。在开发过程中确实有机会做到这一点。只是没割到。
- @里克蒂希:2.7在3.1和3.2之间,而不是3.3左右。这意味着当最后一个对3.2的更改进入时2.7在rc中,这使得bug注释更容易理解。不管怎样,我认为他们在回顾时犯了一些错误(特别是假设人们会通过2to3而不是通过双版本代码在six之类的库的帮助下迁移,这就是为什么我们得到了dict.viewkeys之类的东西而没有人使用的原因),并且在3.2中有一些变化来得太晚了,但是对于OST第2.7部分是一个令人印象深刻的"最后2.x有史以来"版本。
其他答案已经很好地解释了这一点,但我想提供另一个实验来说明靶场物体的性质:
1 2 3 4 5 6 7 8 9
| >>> 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对象是一个记住其范围的对象,可以多次使用(即使是在对其进行迭代时),而不仅仅是一个一次性生成器。
- 我不会把相似性夸大到分割物体太多;这似乎会让更多的人困惑,而不是帮助他们。(一旦有人意识到这一点,他们就会开始纳闷,为什么一开始有两种不同的类型,需要一点时间向他们解释。然后他们中的一些人去设计其他语言,比如Swift,却从来没有弄清楚,我们遇到了各种令人讨厌的问题,在他们最终想出一个合理的设计之前,他们来回地进行5次beta测试…)
- @你认为我应该删除那部分吗?我从来没有显式地创建切片对象。我添加它是因为它们很相似,至少对我来说,切片对象是"静态"的感觉更直观,因为数据不是来自它们,而是来自它们使用的序列。
- 我认为你的直觉肯定有助于让事情变得更清楚……但我不知道你是否能把它理解为答案。可能通过显式调用indices方法?但在那一点上,也许它离重点太远了?
- @abarnetnah,因为我从来没有明确使用过slice对象,我甚至不知道indexs方法,我现在不想考虑它。我就把那个部分从答案中去掉。
这一切都是关于对range的评估和一些额外优化的懒惰方法。在实际使用之前,不需要计算范围中的值,或者由于额外的优化而进一步计算。
顺便说一句,你的整数不是这么大,以sys.maxsize为例。
sys.maxsize in range(sys.maxsize)相当快
由于优化-很容易将给定整数与最小和最大范围进行比较。
但是:
float(sys.maxsize) in range(sys.maxsize)相当慢。
(在这种情况下,range中没有优化,因此如果python收到意外的float,python将比较所有的数字)
您应该知道一个实现细节,但不应该依赖它,因为这在将来可能会改变。
在C#中也有类似的实现。您可以看到在O(1)时间内Contains是如何完成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public struct Range
{
private readonly int _start;
private readonly int _stop;
private readonly int _step;
//other methods/properties omitted
public bool Contains(int number)
{
// precheck: if the number isn't in a valid point, return false
// for example, if start is 5 and step is 10, then it's impossible for 163 to be in range (due to modulo)
if ((_start % _step + _step) % _step != (number % _step + _step) % _step)
return false;
// v is vector: 1 means positive step, -1 means negative step
// this value makes final checking formula straightforward.
int v = Math.Abs(_step) / _step;
// since we have vector, no need to write if/else to handle both cases: negative and positive step
return number * v >= _start * v && number * v < _stop * v;
}
} |
DR
range()返回的对象实际上是range对象。这个对象实现了迭代器接口,这样您就可以像生成器一样按顺序迭代它的值,但它也实现了__contains__接口,当对象出现在in运算符的右侧时,它实际上就是被调用的接口。__contains__()方法返回一个bool,说明该项是否在对象中。由于range对象知道它们的边界和步幅,所以在o(1)中很容易实现。