为什么在Python 3中range(0) == range(2, 2, 2)成立?

74
在Python 3中,为什么使用不同值初始化的范围会相互比较相等?当我在解释器中执行以下命令时:
>>> r1 = range(0)
>>> r2 = range(2, 2, 2)
>>> r1 == r2
True

结果是 True。为什么会这样?为什么具有不同参数值的两个不同的 range 对象被视为相等?


1
一个简短但是在我看来足够的答案是“因为两个范围是相等的”。请注意is==之间的区别。 - Jasper
5个回答

87

range对象是特殊的:

Python将比较range对象作为序列。这实际上意味着比较不评估它们如何表示给定的序列,而是评估它们代表什么。

事实上,startstopstep参数完全不同并不重要,因为当它们扩展时都代表一个空列表:

例如,第一个range对象:

list(range(0))  # []

还有第二个range对象:

list(range(2, 2, 2)) # []
< p >< em >两者都代表一个空列表,由于两个空列表比较相等(True),因此表示它们的 range 对象也将相等。

因此,您可以拥有完全不同的外观 range 对象;如果它们表示相同的序列,则它们将比较相等:

range(1, 5, 100) == range(1, 30, 100) 

两者都表示只有一个元素[1]的列表,因此这两个也相等。


不过,range对象确实非常特殊:

请注意,即使比较不评估它们表示序列的方式,但可以仅使用range对象的startstep以及len的值来实现比较结果。这对于比较速度具有非常有趣的影响:

r0 = range(1, 1000000)    
r1 = range(1, 1000000)

l0 = list(r0)    
l1 = list(r1)

Ranges比较超级快:

%timeit r0 == r1
The slowest run took 28.82 times longer than the fastest. This could mean that an intermediate result is being cached 
10000000 loops, best of 3: 160 ns per loop

另一方面,这些列表...
%timeit l0 == l1
10 loops, best of 3: 27.8 ms per loop

是的..


正如@SuperBiasedMan所指出的,这仅适用于Python 3中的range对象。Python 2的range()是一个返回列表的普通函数,而2.x xrange对象没有与range对象在Python 3中具有的比较能力(不仅仅是这些..)。

查看@ajcr的答案,直接引用Python 3 range对象的源代码。文档记录了两个不同范围之间的比较实际上包括什么:简单快速的操作。在range_richcompare函数中,range_equals函数用于EQNE情况,并分配给PyRange_Type类型的tp_richcompare

我认为range_equals的实现非常可读(因为它很简单)在这里添加:

/* r0 and r1 are pointers to rangeobjects */

/* Check if pointers point to same object, example:    
       >>> r1 = r2 = range(0, 10)
       >>> r1 == r2
   obviously returns True. */
if (r0 == r1)
    return 1;

/* Compare the length of the ranges, if they are equal 
   the checks continue. If they are not, False is returned. */
cmp_result = PyObject_RichCompareBool(r0->length, r1->length, Py_EQ);
/* Return False or error to the caller
       >>> range(0, 10) == range(0, 10, 2)  
   fails here */
if (cmp_result != 1)
    return cmp_result;

/* See if the range has a lenght (non-empty). If the length is 0
   then due to to previous check, the length of the other range is 
   equal to 0. They are equal. */
cmp_result = PyObject_Not(r0->length);
/* Return True or error to the caller. 
       >>> range(0) == range(2, 2, 2)  # True
   (True) gets caught here. Lengths are both zero. */
if (cmp_result != 0)
    return cmp_result;

/* Compare the start values for the ranges, if they don't match
   then we're not dealing with equal ranges. */
cmp_result = PyObject_RichCompareBool(r0->start, r1->start, Py_EQ);
/* Return False or error to the caller. 
   lens are equal, this checks their starting values
       >>> range(0, 10) == range(10, 20)  # False
   Lengths are equal and non-zero, steps don't match.*/
if (cmp_result != 1)
    return cmp_result;

/* Check if the length is equal to 1. 
   If start is the same and length is 1, they represent the same sequence:
       >>> range(0, 10, 10) == range(0, 20, 20)  # True */
one = PyLong_FromLong(1);
if (!one)
    return -1;
cmp_result = PyObject_RichCompareBool(r0->length, one, Py_EQ);
Py_DECREF(one);
/* Return True or error to the caller. */
if (cmp_result != 0)
    return cmp_result;

/* Finally, just compare their steps */
return PyObject_RichCompareBool(r0->step, r1->step, Py_EQ);

我在这里也加入了一些自己的评论;参考@ajcr的答案,查看Python等效内容。


1
它是否也会与具有相同内容的“list”相等?这是否有保证,还是仅仅是实现的有趣副作用? - Mark Ransom
2
@MarkRansom,它并不相等,序列类型之间的相等性会导致不相等。我相信在比较方面的参考手册中已经提到了这一点,我会尝试找到相关部分并将其添加到答案中。 - Dimitris Fasarakis Hilliard
3
请注意,这些都只适用于Python 3。在Python 2中,range返回一个普通的列表,而xrange返回一个xrange类型对象,它不进行智能比较。xrange(0) == xrange(2, 2, 2)返回False - SuperBiasedMan

17

来自文档的直接引用(强调我的):

使用==和!=测试区间对象的等价性将它们视为序列进行比较。也就是说,如果两个range对象表示相同的值序列,则它们被认为是相等的。(请注意,比较相等的两个range对象可能具有不同的start、stop和step属性,例如range(0) == range(2, 1, 3)或range(0, 3, 2) == range(0, 4, 2))。

如果将range与“相同”的列表进行比较,则会得到不相等的结果,正如文档中所述:

除了不同的数字类型外,不同类型的对象永远不会相等。

例如:

>>> type(range(1))
<class 'range'>
>>> type([0])
<class 'list'>
>>> [0] == range(1)
False
>>> [0] == list(range(1))
True

请注意,此处仅适用于Python 3。在Python 2中,range返回一个列表,在该版本中,range(1) == [0] 的结果为True


13

为了在这个页面上的优秀回答中添加一些额外的细节,两个range对象r0r1进行比较,大致如下

if r0 is r1:                 # True if r0 and r1 are same object in memory
    return True
if len(r0) != len(r1):       # False if different number of elements in sequences
    return False
if not len(r0):              # True if r0 has no elements
    return True
if r0.start != r1.start:     # False if r0 and r1 have different start values
    return False
if len(r0) == 1:             # True if r0 has just one element
    return True
return r0.step == r1.step    # if we made it this far, compare step of r0 and r1

使用 startstopstep 参数可以轻松地计算出 range 对象的长度。例如,当 start == stop 时,Python 可以立即知道长度为0。在非平凡的情况下,Python 只需使用 startstopstep 值进行 简单算术计算

因此,在 range(0) == range(2, 2, 2) 的情况下,Python 执行以下操作:

  1. 看到 range(0)range(2, 2, 2) 在内存中是不同的对象。
  2. 计算两个对象的长度; 两个长度都为0(因为在两个对象中,start == stop),所以需要进行另一个测试。
  3. 发现 len(range(0)) 为0。这意味着 len(range(2, 2, 2)) 也为0(之前的不等式测试失败了),因此比较应该返回 True

刚刚编辑了我的答案,添加了这个内容,本来还想加上源代码的引用,但现在我可以将它链接到你的答案中。 - Dimitris Fasarakis Hilliard
感谢链接,@Jim。你的回答(以及其他人的回答)完美地回答了这个问题 - 我的回答只是一个脚注,旨在让可能对CPython实现感到好奇的人们了解一下。 - Alex Riley
是的,我就是其中一个感兴趣的人,当我注意到有一个新答案时,hg.python标签页确实是打开着的。现在不需要再添加了,你已经涵盖了它 :-) - Dimitris Fasarakis Hilliard
在第二点中,您的意思是“两个长度都相同,因此需要进行另一个测试”吗? - gnasher729
@gnasher729:没错,就是这样。谢谢,我会编辑以便更加清晰明了。 - Alex Riley

7

res = range(0) == range(2, 2, 2)

其中:

range(0)

这里的范围是从00,步长为1(默认值),是一个没有值的列表。

range(2, 2, 2)

这意味着范围从22,步长为2,列表中没有值。

因此,这些范围实际上是相等的。


7

range(0) 返回 range(0,0)。你从0开始,到0结束,步长为1,这是未定义的,因为默认情况下第三个参数不能为0。你不能通过步长为1到达0。没有计数器的操作,因此返回0。

range(2, 2, 2) 返回 range(2, 2, 2)。你从2开始,到2结束,但步长为2。这实际上就相当于0,因为你不需要计数到任何东西。

range(0) == range(2,2,2) 

真实完全相同


2
range(2,2,2) 绝不是“基本上为0”。 - Jasper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接