dbouz
dbouz

Reputation: 919

Why do the enumerate, zip, range types not belong to types.GeneratorType?

Python 3 has introduced generator-like objects to be returned upon calling range() and zip(). The object returned acts like a generator and can be iterated through once but doesn't 'print' well, much like the enumerate() return argument.

I was perplexed to see, however, that they are distinct object types and do not belong to types.GeneratorType, or at least this is what the types module shows. A function that would run e.g. expecting a generator would not detect them. What is their inheritance? Do they belong to a main "generator" structure, so that they e.g. could be identified along with other generators?

import types

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

# create some generator-type objects
obj_zip = zip(a,b)
obj_enu = enumerate(a)
obj_r = range(10)

print(type(obj_zip))
print(type(obj_enu))
print(type(obj_r))

# checking against types.GeneratorType returns False
print(isinstance(obj_zip,types.GeneratorType))
print(isinstance(obj_enu,types.GeneratorType))
print(isinstance(obj_r,types.GeneratorType))

# checking against their own distinct object types returns True
print(isinstance(obj_zip,zip))

Upvotes: 2

Views: 473

Answers (3)

Jean-François Fabre
Jean-François Fabre

Reputation: 140276

not meant to be a full answer (ShadowRanger answer already explains everything) but just to state that types.GeneratorType is really a very limited type as shown in types.py source:

def _g():
    yield 1
GeneratorType = type(_g())

it only scopes the type of generator functions. Other "generator-like" objects don't use yield so they're not a match.

Upvotes: 0

ShadowRanger
ShadowRanger

Reputation: 155536

Per the GeneratorType docs:

types.GeneratorType

The type of generator-iterator objects, created by generator functions.

Generator functions are a specific thing in the language; it means functions that use yield or yield from (or generator expressions, which are just a shorthand for inline generator functions). It's a subset of the set of iterators (all things that you can call next() on to get a new value), which is in turn a subset of iterables (all things that you can call iter() on to get an iterator; iterators themselves are iterables, where iter(iterator) behaves as the identity function).

Basically, if you're testing for "can I loop over this?", test isinstance(obj, collections.abc.Iterable). If you're checking "is this an exhaustible iterator?" (that is, will I exhaust it by looping over it?), test either isinstance(obj, collections.abc.Iterator) or for the duck-typing based approach, test iter(obj) is obj (the invariants on iterators require that iter(iterator) yield the original iterator object unchanged).

Note that range is not a generator or iterator. Per the docs:

Rather than being a function, range is actually an immutable sequence type, as documented in Ranges and Sequence Types — list, tuple, range.

Being an immutable sequence type means it is an iterable, but that's it. The fact that it is usually used as if it were an iterator is irrelevant; if it were an iterator, the second loop here would never execute:

r = range(3)
for i in r:
    print("First", i)
for i in r:
    print("Second", i)

but it works just fine, because each (implicit) call to iter(r) returns a new iterator based on the same underlying iterable.

Upvotes: 5

tback
tback

Reputation: 11561

The documentation says that enumerate is functionally equivalent to a generator. Actually it is implemented in C and returns an iterator, not a generator as described in Does enumerate() produce a generator object. Generators and iterables are almost the same. This is explained in detail in Difference between Python's Generators and Iterators.

I'm assuming you're trying to solve a real problem, like finding out if you can iterate over something. To solve that you can test if something is an instance of collections.Iterable.

a = enumerate([1,2,3])
isinstance(a, collections.Iterable)
>>> True

Upvotes: 0

Related Questions