a = 10
while a > 0:
    print(a)
    a -= 1

for a in range(0,10):
    print(a)

[print(a) for a in range(0,10)]

"""
Explanation: Python Training - Lesson 6 - Iterables, Iteration, Generators, Generator Expressions
Iteration
Examples of what iteration can look like
End of explanation
"""


for i in range(0,500):
    if i%376==0:
        print("Got it!")
        break

"""
Explanation: Silly example
Let's say you need to stop your loop when you have a specific condition.
End of explanation
"""


from random import randint

divisor = randint(1,500)

for i in range(1,500):
    if i%divisor==0:
        print("Got it: " + str(i))
        break

"""
Explanation: .... and sometimes the condition changes?
End of explanation
"""


# Normal method of using collections - just generate all at once
a = [1,2,3,4,5]

# Let's change the list into an ITERATOR
my_iterator = iter(a)
print(type(my_iterator))
print(my_iterator)

# Let's iterate the iterator
next(my_iterator)

# And some more:
next(my_iterator)
next(my_iterator)
next(my_iterator)
next(my_iterator)

# ... and more:
next(my_iterator)

"""
Explanation: It seems we don't need to generate the big list at all. Imagine it took some time, like 0.5 seconds for each element to check. Do we need to have them all before we check? 
We need a lazy evaluation, not eager.
Am I done yet? No? Then give me the next one. Just the one.
Iteration protocol
End of explanation
"""


some_list = [1,2,3,4,5,5]
some_iter = iter(some_list)

for x in some_iter:
    print(x)

"""
Explanation: Woops! When iterator is done, it always throws a STOP sign.
A better approach is to just iterate over the iterator.
End of explanation
"""


class lazy_range:
    def __init__(self):
        self.counter = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        self.counter += 1
        return self.counter
        

# Something like iter(some_list)

my_lazy_range = lazy_range()
next(my_lazy_range)

for x in lazy_range():
    print(x)
    if x >10:
        break

"""
Explanation: What's going on under the hood?
Let's try and implement something like that. A lazy range.
To mimic the behavior of "next"  and  "iter", we implement such methods ourselves, but use the built-in Python functions with two underscores.
End of explanation
"""


def laziest_range(N):
    i = 0
    while i<N:
        yield i
        i += 1

a = laziest_range(5)

next(a)

next(a)

for x in laziest_range(10):
    print(x)

"""
Explanation: That looks terrible. Is there an easy way?
Generators
End of explanation
"""


laziest_shortest_range = (x for x in range(10))

next(laziest_shortest_range)

next(laziest_shortest_range)

"""
Explanation: Generator expressions
End of explanation
"""


a = [x for x in range(0,100000)]
b = (x for x in range(0,100000))

import sys
print(sys.getsizeof(a))
print(sys.getsizeof(b))

"""
Explanation: Comparison of sizes
End of explanation
"""


def semaphore():
    print("You go")
    yield
    print("No, you go")
    yield
    print("No, really, you go!")
    yield

a = semaphore()

next(a)

next(a)

next(a)

"""
Explanation: Meaning of 'yield'
End of explanation
"""


from time import sleep
from random import randint

def get_latest_customers():
    rv = []
    for i in range(10):  
        sleep(.5)
        rv.append(randint(1,20))
    return rv


customer_id = 18

# We gather lot's of customer ID's and then check if our customer ID=19 is there.

print(customer_id in get_latest_customers())

def get_latest_customers_lazy(n):
    for i in range(n):  
        sleep(.5)
        yield(randint(1,20))

iterator = get_latest_customers_lazy(2000000000)

for x in iterator:
    if customer_id is x:
        print("Found him!")
        break
    else:
        print("Still looking..." + str(x))

"""
Explanation: Examples with time
End of explanation
"""


