Some people think that Python attaches some special syntax between for
and range
, but range
is an object that supports iteration.
This means that it has an __iter__
function that returns an iterator, and this supports the iterator protocol. This is a protocol that allows us to enumerate items out of that object. Like we can also enumerate over a list, tuple, set, etc.
for
on the other hand simply uses this protocol such that for each iteration, it aims to fetch the next element out of the iterator. In case there is a next element, it assigns that element to the variable(s) on the left side. In case the iterator is exhausted (has no elements anymore), it stops.
In case you construct a range(start,stop)
object where stop <= start
. That range object is considered to be empty: it simply does not enumerate any items. for
does not know what it is enumerating, but since the iterator over the range(..)
element simply says that there are no elements anymore for
stops.
You might indeed argue that it would be better if range(..)
raises an error in case the stop
is less than or equal to start
. But usually it is wanted behavior that no error is risen, and the loop simply does not execute.
You can compare for i in range(0,n)
to a construct in the Java/C++ language family:
for(int i = 0; i < n; i++) {
//...
}
In this case if n
is less than or equal to zero, the loop will not be executed either. The range(..)
object somewhat a generator for such type of loops.
A extra note is that range
actually is more sophisticated than described here in this answer, since you can also add a step
, and you can countdown. But this has no impact on the semantics of this answer.
You can make the code more elegant, by using a generator in an all
statement. Like:
def main(number):
return all(number % i for i in range(2,int(number**0.5)+1))
Furthermore we can boost performance a bit, by only checking odd divisors (and 2):
def main(number):
sq = int(number ** 0.5) + 1
return (number & 1) and all(number % i for i in range(3, n, 2))