0

How does the range() differentiate the call being made in this case?

Example:

def ex():
    list = [1,2,3,4]
    for val in range(len(list)):
        print(val)
        break
    for val in range(len(list)):
        print(val)
        break

Output -

0
0

In short, my question is why doesn't the output yield this way?

0
1

During the first call to the range() in the 'first for loop' , the call is 'range(len(list))', and in the first call to the range() in the 'second for loop', the call is 'range(len(list))' which the equivalent to the second call to the range() in the 'first for loop'. How does range() know if the call was from 'second for loop' and not 'first for loop'?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
itsme.cvk
  • 123
  • 1
  • 8
  • `range()` with 1 arg, returns an iterator that starts at 0 and increments until `arg`. It is not repeatedly called by the first for loop. You are then breaking out of the first loop so starting a fresh loop that makes another call to `range()` that returns an iterator that starts at 0. – AChampion Oct 08 '16 at 17:06
  • Why would it yield that output? Loops don't start where the other left off, and you break on the first iteration. – Andrew Li Oct 08 '16 at 17:07
  • The answer (to the question in your title) depends on which version of Python you are using. Python 3 `range` differs from Python 2's. Also -- don't use `list` as an identifier. It overwrites a built-in name. – John Coleman Oct 08 '16 at 17:13
  • However, the results are the same in both python versions. – AChampion Oct 08 '16 at 17:13
  • `range(len(list))` in this case yields, `range(0, 4)`. But, what does range(0, 4) yield? – itsme.cvk Oct 08 '16 at 17:56

1 Answers1

6

I'm not sure why you expect range would remember that it had been called previously. The class does not maintain any state about previous calls; it simply does what you ask. Each call to range(x) returns a new range object that provides numbers from 0 to x-1 as you iterate over it. Each call is independent of any previous calls.

To get the behavior you are describing, you need to reuse the same iterator for the range object in each loop.

Python 3.5.1 (default, Apr 18 2016, 11:46:32)
[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> l = [1,2,3,4]
>>> r = range(len(l))
>>> for val in r:
...   print(val)
...   break
...
0
>>> for val in r:
...   print(val)
...   break
...
0
>>> i = iter(r)
>>> for val in i:
...   print(val)
...   break
...
0
>>> for val in i:
...   print(val)
...   break
...
1

You can image that something like

for x in xs:
    do_something(x)

is short for

i = iter(xs)
while True:
    try:
        x = next(i)
    except StopIteration:
        break
    do_something(x)

iter returns its argument if it is already, in fact, an iterator, so every for loop returns a fresh, start-at-the-beginning iterator when you attempt to iterate over a non-iterator iterable.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • each call returns a new Iterator object initialized with the given sequence (0, 1, ...). This iterator is the "range object" noted, but I think the correct term is Iterator. The for loop calls the "next" function to actually iterate over the sequence. – DanJ Oct 08 '16 at 17:23
  • 1
    The `range` object itself is not an iterator; using `r` in both loops still produces the output in the question, because each `for` loop fetches a new iterator from the `range` object (via `r.__iter__`). – chepner Oct 08 '16 at 17:24
  • 3
    An iterator is something that provides a `__next__` method, while an *iterable* is something that provides an `__iter__` method. `range` defines `__iter__`, but not `__next__`. (`range.__iter__` returns an instance of `range_iterator`, which is the class that defines `__next__`). – chepner Oct 08 '16 at 17:33
  • @chepner I did not know how to express my question exactly. I think we are getting there from your last comment. Can you explain me your last comment in a simpler way? I do not understand what makes 'range' (in your answer) produce different output than 'iter' ( in your answer)? I was expecting both to behave the same. – itsme.cvk Oct 08 '16 at 17:50
  • See [What exactly are Python's iterator, iterable, and iteration protocols?](http://stackoverflow.com/q/9884132/1126841). (I appear to have reversed the meanings of *iterator* and *iterable* here.) – chepner Oct 08 '16 at 18:35
  • 2
    Ignoring the exact implementation, the key thing to understand is that Python distinguishes between an object that *can* be iterated over and the state of an in-progress iteration. `range` represents a static sequence of numbers; `range_iterator` represents the process of iterating over a range. You can have multiple active iterators that iterate over the same `range` object, with each one having its own idea of the "current" value being looked at. – chepner Oct 08 '16 at 18:45