3

Python3's range objects support O(1) containment checking for integers (1) (2)

So one can do 15 in range(3, 19, 2) and get the correct answer True

However, it doesn't support containment checking b/w two ranges

a = range(0, 10)
b = range(3, 7)
a in b # False
b in a # False, even though every element in b is also in a
a  < b # TypeError: '<' not supported between instances of 'range' and 'range'

It seems that b in a is interpreted as is any element in the range 'a' equal to the object 'b'?

However, since the range cannot contain anything but integers, range(...) in range(...) will always return False. IMHO, such a query should be answered as is every element in range 'b' also in range 'a'? Given that range only stores the start, stop, step and length, this query can also be answered in O(1).

The slice object doesn't help either. It doesn't implement the __contains__ method, and the __lt__ method simply compares two slices as tuples (which makes no sense)

Is there a reason behind the current implementation of these, or is it just a "it happened to be implemented this way" thing?

Pragy Agarwal
  • 385
  • 4
  • 22

3 Answers3

3

It looks like the implementation of __contains__ for ranges is range_contains, which just checks if the given element is in the iterable, with a special case for longs.

As you have correctly observed, e in b returns true iff e is an element in b. Any other implementation, such as one that checks if e is a subset of b, would be ambiguous. This is especially problematic in Python, which doesn't require iterables be homogenous, and allows nested lists/tuples/sets/iterables.

Consider a world where your desired behavior was implemented. Now, suppose we have the following list:

my_list = [1, 2, (3, 4), 5]
ele1 = (3, 4)
ele2 = (2, 5)

In this case, what should ele1 in my_list and ele2 in my_list return? Your desired behavior makes it tedious to write code such as

if e in my_iterable:
    # ah! e must exist!
    my_iterable.remove(e)

A safer, better way is to keep the current behavior, and instead use a different type-sensitive operator to implement subset predicates:

x = set([1])
y = set([1,2])
x < y  # True
[1] < y  # raises a TypeError
James Lim
  • 12,332
  • 3
  • 37
  • 63
  • Well, range isn't a container, but a sequence which only represents integer ranges. Although it is iterable, removing elements from it isn't allowed. So checking containment with a non number type can still be done. But you're right, it will create a lot of confusion and the cons outweigh the pros. – Pragy Agarwal Dec 17 '17 at 13:45
1

You're confusing 'b' containing 'a' with 'a' being a subset of 'b' - These are two different things.

b containing a means range(0, 10) is inside b. Let's say:

a = [1, 2, 3]

and

b = [1, 2, 3, 4, 5]

a in b is only true if the actual list [1, 2, 3] is in [[1, 2, 3], 4, 5]. So you're actually checking if the list itself is inside the other list, not that all the elements are in the other list.

A list a is a subset of b if all elements of a are inside b. In your example, b is a subset of a, yes, but the actual list b is not IN a.

If you want to do such methods, then it's probably recommended that you use a set data structure

TerryA
  • 52,957
  • 10
  • 101
  • 125
1

Range objects implement the collections.abc.Sequence, It supports containment tests.

a in b 
b in a

In this case, you are searching for Range object a in range b, vice versa. It should be false.

not_python
  • 804
  • 6
  • 13