# import python packages here...

"""
Explanation: Python's iterators and generators
TODO
* https://stackoverflow.com/questions/2776829/difference-between-pythons-generators-and-iterators
* https://docs.python.org/3/glossary.html#term-generator
* https://docs.python.org/3/glossary.html#term-generator-iterator
* https://docs.python.org/3/reference/simple_stmts.html#yield
* https://docs.python.org/3/library/stdtypes.html#iterator-types
* https://docs.python.org/3/library/functions.html#next
* https://docs.python.org/3/glossary.html#term-iterable
* https://docs.python.org/3/glossary.html#term-iterator
* https://docs.python.org/3/library/stdtypes.html#iterator.next
* https://docs.python.org/3/library/functions.html#iter
* https://docs.python.org/3/library/exceptions.html#StopIteration
* https://docs.python.org/3/reference/datamodel.html#object.iter
* https://docs.python.org/3/library/stdtypes.html#typeiter
* https://wiki.python.org/moin/Generators
* https://wiki.python.org/moin/Iterator
End of explanation
"""


class Counter:
    
    def __init__(self, max_value):
        self.current_value = 0
        self.max_value = max_value
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current_value >= self.max_value:
            raise StopIteration

        self.current_value += 1
        
        return self.current_value

###
 
cpt = Counter(10)   # cpt is an iterator

for i in cpt:
    print(i)

###

cpt = Counter(10)   # cpt is an iterator
print(next(cpt))
print(next(cpt))

###

print([i for i in cpt])

"""
Explanation: Iterator
TODO...
End of explanation
"""


def counter(max_value):
    current_value = 0
    while current_value < max_value:
        yield current_value    # temporarily suspends processing, returning i and remembering the location execution state (including local variables and pending try-statements)
        current_value += 1

for elem in altrange(10):
    print(elem)

    
list(altrange(10))

def gen(nmax):    # "gen" is a "generator"
    n = 0
    while n<nmax:
        yield n
        n += 1

gi = foo()        # "gi" is a "generator iterator"

for n in gi;
    print(n)

"""
Explanation: Generator
Goal: increase memory efficiency of some functions. See https://wiki.python.org/moin/Generators.
Generator (or generator function)
https://docs.python.org/3/glossary.html#term-generator
A function which returns a generator iterator. It looks like a normal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function.
Usually refers to a generator function, but may refer to a generator iterator in some contexts. In cases where the intended meaning isn't clear, using the full terms avoids ambiguity.
End of explanation
"""


altrange(10)

"""
Explanation: Generator iterator
https://docs.python.org/3/glossary.html#term-generator-iterator
An object created by a generator function.
Each yield temporarily suspends processing, remembering the location execution state (including local variables and pending try-statements). When the generator iterator resumes, it picks-up where it left-off (in contrast to functions which start fresh on every invocation).
End of explanation
"""


(i*i for i in range(10))

list((i*i for i in range(10)))

list(i*i for i in range(10))

sum(i*i for i in range(10))         # sum of squares 0, 1, 4, ... 81

"""
Explanation: Generator expression
https://docs.python.org/3/glossary.html#term-generator-expression
An expression that returns an iterator. It looks like a normal expression followed by a for expression defining a loop variable, range, and an optional if expression. The combined expression generates values for an enclosing function:
End of explanation
"""


class AltRange:
    def __init__(self, n):
        self.i = 0
        self.n = n
        
    def run(self):
        self.i = 0
        while self.i < self.n:
            yield self.i    # temporarily suspends processing, returning i and remembering the location execution state (including local variables and pending try-statements)
            self.i += 1

obj = AltRange(10)
obj.run()

list(obj.run())

for elem in obj.run():
    obj.i += 1
    print(elem)

"""
Explanation: Strange test: does it memorize the object state ? => that's a stupid question as the state of an object is always "memorized"... This only concerns function's internal state (i.e. yield only memorize the state of the function it is defined in, not the objects state)...
End of explanation
"""


