我想检查给定的x
是否位于区间[0,a-1]
中。 作为一个懒惰的程序员,我写了
x in range(a)
并且(因为该段代码位于 4.5 嵌套循环中)很快就会遇到性能问题。我测试了它,事实上,事实证明n in range(n)
的运行时在于 O(n),给予或接受。我实际上认为我的代码会优化为x >= 0 and x < a
但似乎并非如此。即使我提前修复了range(a)
,时间也不会变得恒定(尽管它改进了很多) - 请参阅旁注。
所以,我的问题是:
我应该使用x >= 0 and x < a
并且永远不再写x in range(a)
吗?有没有更好的写法?
旁注:
- 我尝试在 SO 中搜索范围、python-2.7、性能标签放在一起,但一无所获(与 python-2.x 相同)。
如果我尝试以下操作:
i = range(a) ... x in i
所以范围是固定的,我只测量
x in i
的运行时间,我仍然以 O(x) 为单位获得运行时间(假设a
足够大)。n in xrange(n)
的运行时间也位于 O(n) 中。- 我找到了这篇文章,它对python 3提出了类似的问题。我决定在python 3上测试同样的东西,它像什么都没有一样通过了测试。我为python 2感到难过。
Python 2 中range
的问题在于它创建了一个值list
,因此x in range(a)
将创建一个列表并线性扫描该列表。xrange
应该是一个发电机,但它并没有快多少;可能仍然只是线性扫描值,只是没有先创建整个列表。
In [2]: %timeit 5*10**5 in range(10**6 + 1) # Python 2
10 loops, best of 3: 18.1 ms per loop
In [3]: %timeit 5*10**5 in xrange(10**6 + 1) # Python 2
100 loops, best of 3: 6.21 ms per loop
在 Python 3 中,range
更加智能,不仅不会创建整个列表,而且还提供了contains
检查的快速实现。
In [1]: %timeit 5*10**5 in range(10**6 + 1) # Python 3
1000000 loops, best of 3: 324 ns per loop
更快,恕我直言,更具可读性:使用比较链接:
In [2]: %timeit 0 <= 5*10**5 < 10**6 + 1 # Python 2 or 3
10000000 loops, best of 3: 46.6 ns per loop
">我应该使用
x >= 0 and x < a
并且永远不再在范围 (a) 中写 x 吗?有没有更好的写法?
不","视情况而定"和"是"。您不应该使用x >= 0 and x < a
因为0 <= x < a
更短且更容易解析(对于弱小的人类),并且被解释为(0 <= x) and (x < a)
。而且你不应该在 Python 2 中使用in range
,但在 Python 3 中,如果你愿意,你可以使用它。
不过,我更喜欢比较链,因为a <= x < b
比x in range(a, b)
更明确地说明边界(如果x == b
呢?),这可以防止许多逐一错误或+1
填充范围。
另外,请注意,0 <= x < a
与x in range(0, a)
并不严格相同,因为range
将永远只包含整数值,即1.5 in range(0, 5)
是False
,而0 <= 1.5 < 5
是True
的,这可能不是你想要的。此外,使用range
您可以使用1
以外的步骤,例如5 in range(4, 10, 2)
是False
,但也可以使用纯数学来实现,例如(4 <= x < 10) and (x - 4 % 2 == 0)
。
通过使用自定义范围类并覆盖in
运算符,可以获得与 python3 中相同的性能。对于微不足道的情况,它的性能不如简单的比较,但是您将避免使用内置range()
或xrange()
获得的O(n)
内存和时间使用量。
请注意,测试value in range(low, high)
与low < value <= high
不同,因为范围将只包含整数。所以7.2 in range(10) == False
.
但更重要的是,range()
可以采用可选的第三步参数,因此如果您需要测试value in range(low, high, step)
,则可以考虑使用自定义类。
编辑:@mike239x找到了future
包,其中包含一个类似于我的答案中的对象的range
对象(除了帮助您编写python2/3交叉兼容代码的其他函数)。使用它应该是安全的,因为它可能经过良好测试和稳定。
此类的对象包装xrange
对象,并且仅重写开销非常大的in
操作。对于常规迭代,它的工作原理就像xrange
.
class range(object):
"""Python 2 range class that emulates the constant time `in` operation from python 3"""
def __init__(self, *args):
self.start, self.end = (0, args[0]) if len(args) == 1 else args[:2]
self.step = 1 if len(args) < 3 else args[2]
self.xrange = xrange(*args)
def __contains__(self, other):
# implements the `in` operator as O(1) instead of xrange's O(n)
try:
assert int(other) == other
except Exception:
return False # other is not an integer
if self.step > 0:
if not self.start <= other < self.end:
return False # other is out of range
else:
if not self.start >= other > self.end:
return False # other is out of range
# other is in range. Check if it's a valid step
return (self.start - other) % self.step == 0
def __iter__(self):
# returns an iterator used in for loops
return iter(self.xrange)
def __getattr__(self, attr):
# delegate failed attribute lookups to the encapsulated xrange
return getattr(self.xrange, attr)
内置的xrange
对象是用 C 实现的,所以我们不能使用类继承。相反,我们可以使用组合并将除__contains__
之外的所有内容委托给封装的xrange
对象。
包含的实现可以与cpythonrangeobject
实现中的range_contains_long
进行比较。这是该函数的python 3.6源代码。
编辑:要获得更全面的python实现,请从future
库中查看future.builtins.range
。
叫x in range( a )
慢?(如果使用range()
,请注意 py2 隐藏的风险 ...
)23[us] spent in [py2] to process ( x in range( 10E+0000 ) )
4[us] spent in [py2] to process ( x in range( 10E+0001 ) )
3[us] spent in [py2] to process ( x in range( 10E+0002 ) )
37[us] spent in [py2] to process ( x in range( 10E+0003 ) )
404[us] spent in [py2] to process ( x in range( 10E+0004 ) )
4433[us] spent in [py2] to process ( x in range( 10E+0005 ) )
45972[us] spent in [py2] to process ( x in range( 10E+0006 ) )
490026[us] spent in [py2] to process ( x in range( 10E+0007 ) )
2735056[us] spent in [py2] to process ( x in range( 10E+0008 ) )
MemoryError
in range( a )
构造函数的语法不仅在[TIME]
域中很慢,如果做得更聪明,则比通过列表值的枚举域进行纯顺序搜索更聪明,而且在
py2中,本机range()
总是具有[TIME]
域成本(构建时间)和[SPACE]
的复合附加O( N )
成本-域成本(分配存储空间+花费更多时间将所有这些数据通过......)这种基于range
的内存表示结构。
让我们对一个安全、O( 1 )
扩展的方法进行基准测试( +始终做基准测试)
>>> from zmq import Stopwatch
>>> aClk = Stopwatch()
>>> a = 123456789; x = 123456; aClk.start(); _ = ( 0 <= x < a );aClk.stop()
4L
>>> a = 123456789; x = 123456; aClk.start(); _ = ( 0 <= x < a );aClk.stop()
3L
评估基于条件的公式需要3 ~ 4 [us]
,具有O(1)缩放,不变到x
量级。
接下来,使用x in range( a )
公式进行测试:
>>> a = 123456789; x = 123456; aClk.start(); _ = ( x in range( a ) );aClk.stop()
并且您的计算机几乎会冻结在内存吞吐量受限的CPU匮乏中(更不用说令人讨厌的交换溢出效应,从大约~ 100 [ns]
几个数量级的成本范围到交换磁盘IO数据流的一些~ 15.000.000 [ns]
成本)。
不 不 不。永远无法测试x
是否在有界范围内。
创建一些其他的,基于类的评估器的想法,仍然通过枚举(集合)来解决问题,将永远无法满足基准3 ~ 4 [us]
(如果不使用一些超出我对经典和量子物理学中因果定律的理解的外星魔法)
Python 3 改变了range()
-constructor 的工作方式,但这不是原始帖子的核心优点:
3 [us] spent in [py3] to process ( x in range( 10E+0000 ) )
2 [us] spent in [py3] to process ( x in range( 10E+0001 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0002 ) )
2 [us] spent in [py3] to process ( x in range( 10E+0003 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0004 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0005 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0006 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0007 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0008 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0009 ) )
2 [us] spent in [py3] to process ( x in range( 10E+0010 ) )
1 [us] spent in [py3] to process ( x in range( 10E+0011 ) )
在 Python 2 中,两者都range()
xrange()
摆脱O( N )
扩展的陷阱,其中xrange()
-generator 的运行速度似乎只降低了 2 倍。
>>> from zmq import Stopwatch
>>> aClk = Stopwatch()
>>> for expo in xrange( 8 ):
... a = int( 10**expo); x = a-2; aClk.start(); _ = ( x in range( a ) );aClk.stop()
...
3L
8L
5L
40L
337L
3787L
40466L
401572L
>>> for expo in xrange( 8 ):
... a = int( 10**expo); x = a-2; aClk.start(); _ = ( x in xrange( a ) );aClk.stop()
...
3L
10L
7L
77L
271L
2772L
28338L
280464L
范围边界语法享有恒定O( 1 )
~ < 1 [us]
时间,如上所述,因此设置了再次比较的标准:
>>> for expo in xrange( 8 ):
... a = int( 10**expo); x = a-2; aClk.start(); _ = ( 0 <= x < a );aClk.stop()
...
2L
0L
1L
0L
0L
1L
0L
1L
所以,是的,基本上,在 Python 2 中使用range
(如前所述)是一个坏主意 - python 实际上创建了一个包含范围所有值的列表 + 之后它以最直接的方式搜索整个列表。
其中一个选项如下:使用Python 3中的range
,由于各种原因,它可以更好地处理这种情况。 "好吧",你问,"如何在Python 2中使用Python 3中的range
"?答案是:使用future
库。安装它,写下来
from future.builtins import range
在你的代码头和wuolah!- 你的范围现在的行为与Python 3中的范围一样,现在你可以再次使用x in range(a)
,没有任何性能问题。