为什么在 Python 3 中 "1000000000000000 in range(1000000000000001)" 运行得如此之快?
为什么在 Python 3 中 "1000000000000000 in range(1000000000000001)" 运行得如此之快?
据我所知,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)
如果我尝试实现自己的range函数,结果就不那么好了!
def my_crappy_range(N): i = 0 while i < N: yield i i += 1 return
range()
对象在内部做了什么使其运行速度如此之快?
选择Martijn Pieters的回答是因为其全面性,但也可以参考abarnert的第一个回答,讨论了Python 3中的range
是一个完整的序列意味着什么,以及有关__contains__
函数优化在Python实现之间可能存在的潜在不一致性的一些信息/警告。abarnert的另一个回答详细介绍了Python 3中优化的背后历史,并提供了有关Python 2中xrange
的优化不足的链接。回答by poke和by wim为有兴趣的人提供了相关的C源代码和解释。
这里的根本误解是认为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>>> 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
没有使用list
的任何特殊功能,除了它提供了__getitem__
的C实现,所以它对于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
仍然在生成器中吗?测试1
是否会导致它迭代并消耗所有值,直到1
(或者大于等于1
的第一个值)?
Python 3中的range()
对象不会立即产生数字;它是一个智能的序列对象,按需产生数字。它只包含您的开始、停止和步长值,然后在迭代对象时每次迭代都会计算下一个整数。
该对象还实现了object.__contains__
钩子,并计算您的数字是否是其范围的一部分。计算是一个(几乎)恒定时间操作*。从不需要扫描范围内所有可能的整数。
来自range()
对象文档:
range
类型相对于普通的list
或tuple
的优点在于,无论它所代表的范围有多大(它仅存储start
、stop
和step
值,根据需要计算单个项和子范围),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
这仍然缺少实际的range()
支持的几个功能(例如.index()
或.count()
方法、哈希、相等性测试或切片),但应该能够给你一个想法。
我还简化了__contains__
的实现,只关注整数测试;如果您向实际的range()
对象提供非整数值(包括int
的子类),则会启动慢速扫描以查看是否存在匹配项,就像您对包含所有包含值的列表进行包含测试一样。这是为了继续支持其他数字类型,它们只是碰巧支持与整数的相等性测试,但不希望也不支持整数算术运算。请参见实施包含测试的原始Python问题。
* 因为Python整数是无界的,所以随着N的增长,数学运算也会增长时间,使得这成为O(log N)的操作。由于它是在优化的C代码中执行的,并且Python将整数值存储在30位块中,因此在此处涉及的整数的大小方面,您会在看到任何性能影响之前耗尽内存。