What is the __len__ of an InfiniteRange?

I have an InfiniteRange class that goes:

class _InfiniteRange:
	
	_start: int
	_step: int
	
	def __init__(self, start: int, step: int, /) -> None:
		self._start = start
		self._step = step
	
	def __iter__(self) -> Iterator[int]:
		yield from count(self._start, self._step)
	
	def __getitem__(self, item: int) -> int:
		if item < 0:
			raise IndexError(item)
		
		return self._start + item * self._step
	
	def __contains__(self, item: object) -> bool:
		if not isinstance(item, int):
			return False
		
		if self._step == 0:
			return item == self._start
		
		quotient, remainder = divmod(item - self._start, self._step)
		
		return quotient >= 0 and remainder == 0

I naturally felt that this needs to support len() since this class is almost a virtual subclass of Sequence (just not Reversible). The only problem is len() expects either an int greater than 0 or anything convertible to such a value. That rules out using math.inf/float('inf'), -1 or even custom types.

What could possibly be a good solution here, aside from leaving __len__ undefined, which is what I’m already doing? This is merely a theoretical question.

raise OverflowError. There is no useful value you can return that isn’t going to mislead a user.

2 Likes

Sequence itself does not support __len__. There is a completely separate ABC (Sized) for representing things that have a size/length. An infinite sequence is not one of them. Just leave __len__ undefined.

I know that, but when we talk about ranges, we often refer to its sequence-like properties and not just its length.

No: collections.abc — Abstract Base Classes for Containers — Python 3.12.1 documentation

Sized is a supertype of Sequence, specifically via Collection.

The fact that Sequence inherits (indirectly) from Sized implies that Sequence represents finite sequences.

The reason I don’t like the idea of raising an exception from __len__ is that any code operating on an arbitrary Sequence value will be just as surprised by any exception your __len__ method might raise as it would be by len(x) raising a TypeError for __len__ not being defined at all.

I think it would just be best to leave your class as sequence-like rather than pretending that it’s a virtual subclass of Sequence. It’s still a virtual subclass of Container and Iterable for what it does define. There is no one ABC that indicates __getitem__ is available, so there’s no reason to think you have to be one of the 4 ABCs that do guarantee it in order to implement it yourself.

2 Likes

Yes, I should have said that a sequence (generic) shouldn’t necessarily support len.

There probably should be an Indexible ABC which indicates support for __getitem__, which Sequence et al subclass. Then you could at least say that InfiniteRange is an indexible iterable container.

For that matter, you could define it yourself, something like

from abc import ABCMeta, abstract method

class Indexible(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __getitem__(self, x):
        pass

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Indexible:
            return _check_methods(C, '__getitem__')
        return NotImplemented

The posted code leaves out from itertools import count. While itertools has a few infinite or possible so iterators, an infinite iterable is rare. If people call len() on something without len, they should get a TypeError to expose the bug.

I think getitem should also accept a slice and then return a range.

Side note: yield from count(...) wastefully wraps the perfectly fine count iterator in an additional generator iterator, slowing it down. Better directly return count(...).

1 Like

Correct, but that has yet to be implemented (and is somewhat irrelevant to the matter at hand too).