Many Python builtin "functions" are actually classes, although they also have a straightforward function implementation. Even very simple ones, such as
while True: yield x
Implementing as a class for
itertools has some advantages that generator functions don't have. For example:
__next__that preserves state as instance attributes;
yieldbased generators are a Python layer nicety, and really, they're just an instance of the
generatorclass (so they're actually still class instances, like everything else in Python)
__deepcopy__(and if it's a Python level class, it probably doesn't even need to do that; it will work automatically) and make the instances pickleable/copyable (so if you have already generated 5 elements from a
rangeiterator, you can copy or pickle/unpickle it, and get an iterator the same distance along in iteration)
For non-generator tools, the reasons are usually similar. Classes can be given state and customized behaviors that a function can't. They can be inherited from (if that's desired, but C layer classes can prohibit subclassing if they're "logically" functions).
It's also useful for dynamic instance creation; if you have an instance of an unknown class but a known prototype (say, the sequence constructors that take an iterable, or
chain or whatever), and you want to convert some other type to that class, you can do
type(unknown)(constructorarg); if it's a generator,
type(unknown) is useless, you can't use it to make more of itself because you can't introspect to figure out where it came from (not in reasonable ways).
And beyond that, even if you never use the features for programming logic, what would you rather see in the interactive interpreter or doing print debugging of
<class 'generator'> that gives no hints as to origin, or
<class 'itertools.repeat'> that tells you exactly what you have and where it came from?