If Range() Is a Generator in Python 3.3, Why How to Not Call Next() on a Range

If range() is a generator in Python 3.3, why can I not call next() on a range?

range is a class of immutable iterable objects. Their iteration behavior can be compared to lists: you can't call next directly on them; you have to get an iterator by using iter.

So no, range is not a generator.

You may be thinking, "why didn't they make it an iterator"? Well, ranges have some useful properties that wouldn't be possible that way:

  • They are immutable, so they can be used as dictionary keys.
  • They have the start, stop and step attributes (since Python 3.3), count and index methods and they support in, len and __getitem__ operations.
  • You can iterate over the same range multiple times.


>>> myrange = range(1, 21, 2)
>>> myrange.start
1
>>> myrange.step
2
>>> myrange.index(17)
8
>>> myrange.index(18)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 18 is not in range
>>> it = iter(myrange)
>>> it
<range_iterator object at 0x7f504a9be960>
>>> next(it)
1
>>> next(it)
3
>>> next(it)
5

Why generator function does not work with the next call?

Each call to getPositiveCount() creates a brand new generator. You are thinking of this:

gen = getPositiveCount()
print(next(gen))
print(next(gen))
print(next(gen))

Does next() eliminate values from a generator?

Actually this is can be implicitly deduced from next's docs and by understanding the iterator protocol/contract:

next(iterator[, default])

Retrieve the next item from the iterator by
calling its next() method. If default is given, it is returned if
the iterator is exhausted, otherwise StopIteration is raised.

Yes. Using a generator's __next__ method retrieves and removes the next value from the generator.

Using next() on generator function

Your function returns a new generator each time you call gen().

I think the simplest way to understand is to contrast what you are doing:

>>> next(gen())
0
>>> next(gen())
0

with this:

>>> my_gen = gen()
>>> next(my_gen)
0
>>> next(my_gen)
1
>>> next(my_gen)
4
>>> next(my_gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

In this latter case, I am getting new values from the same generator.

Why does this generator work with a for loop but not with next()?

You need to latch the generator to a variable and next through that so youre going through the same instance, otherwise you're going through a new instance each loop, hence you're getting 0,0,0

def generador():
for i in range(3):
for j in range(3):
for k in range(3):
yield i,j,k
a = generador()
for _ in range(27):
print(next(a))

Why is the range object not an iterator?

range returns an iterable, not an iterator. It can make iterators when iteration is necessary. It is not a generator.

A generator expression evaluates to an iterator (and hence an iterable as well).

How does range object allow for multiple iterations while generators object do not?

A range object is a plain iterable sequence, while a generator is also an iterator.

The difference between the two is that an iterable is used to generate iterators which store the iteration state. This can be seen if we play around with range, its iterators, and next a bit.

First, we can see that range is not an iterator if we try to call next on it

In [1]: next(range(0))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [1], in <module>
----> 1 next(range(0))

TypeError: 'range' object is not an iterator

We can create the iterator ourselves by calling the iter builtin, we can see that this gives us a different iterator type when called on our range.

In [2]: iter(range(0))
Out[2]: <range_iterator at 0x28573eabc90>

Each of the iterators created by the iterable will store its own iteration state (say, an index into the range object that's incremented every time it's advanced) so we can use them independently

In [3]: range_obj = range(10)

In [4]: iterator_1 = iter(range_obj)

In [5]: iterator_2 = iter(range_obj)

In [6]: [next(iterator_1) for _ in range(5)] # advance iterator_1 5 times
Out[6]: [0, 1, 2, 3, 4]

In [7]: next(iterator_2) # left unchanged, fetches first item from range_obj
Out[7]: 0

Python also creates iterators by itself when a for loop is used, which can be seen if we take a look at instructions generator for it

In [8]: dis.dis("for a in b: ...")
1 0 LOAD_NAME 0 (b)
2 GET_ITER
>> 4 FOR_ITER 4 (to 10)
6 STORE_NAME 1 (a)
8 JUMP_ABSOLUTE 4
>> 10 LOAD_CONST 0 (None)
12 RETURN_VALUE

Here, the GET_ITER is the same as doing iter(b).

Now with the generator, after creating it by calling the generator function, Python gives you an iterator directly, as there's no iterable object above it to be generated from. Calling the generator function could be seen as calling iter(...), but passing it everything is left up to the user
as arguments to the function instead of fetching the information from an object it was created by.

When I create a class based generator, why do I have to call the next?

Your __next__ method itself is a generator function due to using yield. It must be a regular function that uses return instead.

def __next__(self):
if self.count < self.max_times:
self.count += 1
return self.value # return to provide one value on call
raise StopIteration # raise to end iteration

When iterating, python calls iter.__next__ to receive the new value. If this is a generator function, the call merely returns a generator. This is the same behaviour as for any other generator function:

>>> def foo():
... yield 1
...
>>> foo()
<generator object foo at 0x106134ca8>

This requires you to call next on the generator to actually get a value. Similarly, since you defined BoundedGenerator.__next__ as a generator function, each iteration step provides only a new generator.

Using return instead of yield indeed returns the value, not a generator yielding said value. Additionally, you should raise StopIteration when done - this signals the end of the iteration.

What does * do with range() in python?

You are executing this, essentially,

n = int(input())
print(*range(1, n + 1), sep='')

1.) The star *args syntax lets you fill in arguments from an iterable. For example, these are all equivalent:

def foo(a, b):
return a + b

foo(1, 2)
lst = [1, 2]
foo(*lst)
tup = (1, 2)
foo(*tup)

2.) Certainly you can use just range without unpacking *args. See next item.

3.) Another way would be to print within a for loop, or more compactly to create a string s:

s = "".join(map(str, range(1, n + 1)))
print(s)

Difference between Python xrange and generators?

Both range and generator objects are examples of iterable objects.

A range is defined by a stopping value, with an optional starting value and optional step size allowed. The result is an iterable of integers. A range also supports operations (like containment) that are not necessarily supported by other iterables.

A generator is defined by a generator expression or a generator function, either of which results in an iterable of arbitrary values.

The iterable aspect of a range object can be simulated by a generator:

def myrange(stop, start=None, step=None):
if start is not None:
from_ = stop
to_ = start
else:
from_ = 0
to_ = stop

if step is None:
step = 1 if from_ < to_ else -1

while from_ < to_:
yield from_
from_ += step

Then

>>> list(myrange(1, 10))
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(1, 10))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

As Python 2 reached end-of-life nearly a year ago, there's not much reason to go into range. Suffice it to say, in Python 2 range was a function that returned a list of integers, while xrange was a type whose value represents a list of integers. Python 3 did away with the function and reused the name for the type.



Related Topics



Leave a reply



Submit