<jupyter_start><jupyter_text>Iterators<jupyter_code>for item in numbers:
    print(item)<jupyter_output>6
4
2
3
2
4
8
5
10
8<jupyter_text>![title](imgs/55.png)![title](imgs/56.png)![title](imgs/57.png)![title](imgs/58.png)![title](imgs/59.png)![title](imgs/60.png)<jupyter_code>class Squares:
    def __init__(self,length):
        self.length = length
        self.i = 0
    
    def __len__(self):
        return self.length
        
    
    def __next__(self):
        if self.i >= self.length:
            raise StopIteration
        else:
            result = self.i**2
            self.i += 1
            return result
        
    def __iter__(self):
        return self
sq = Squares(3)
for i in sq:
    print(i)
for i in sq:
    print(i)
next(sq)
sq1 = Squares(5)
for i in sq1:
    print(i)
l = ['a','b','c']
list(enumerate(l))<jupyter_output><empty_output><jupyter_text>Iterators and Iterables ![title](imgs/68.png)![title](imgs/61.png)![title](imgs/62.png)![title](imgs/63.png)![title](imgs/64.png)![title](imgs/65.png)![title](imgs/66.png)![title](imgs/67.png)<jupyter_code>class Cities:
    def __init__(self):
        self._cities = ['Paris', 'Berlin','New York', 'London']
        self._index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self._index >= len(self._cities):
            raise StopIteration
        else:
            item = self._cities[self._index]
            self._index += 1
            return item
cities = Cities()
type(cities)
list(enumerate(cities))
cities = Cities()
[item.upper() for item in cities]
# To DO ABOVE WAY IS MEMORY INIEFFICIENT SO IT'S BETTER TO SEPARATE ITERATOR AND CITIES CLASS
class Cities:
    def __init__(self):
        self._cities = ['Paris', 'Berlin','New York', 'London']
        self._index = 0
    
    def __len__(self):
        return len(self._cities)
cities = Cities()
len(cities)
class CityIterator:
    def __init__(self, city_obj):
        self._city_obj = city_obj
        self._index = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self._index >= len(self._city_obj):
            raise StopIteration
        else:
            item = self._city_obj._cities[self._index]
            self._index += 1
            return item
cities = Cities()
city_iterator = CityIterator(cities)
for city in city_iterator:
    print(city)
# Now it is exhausted, we need to create another iterator
for city in city_iterator:
    print(city)
city_iterator = CityIterator(cities)
for city in city_iterator:
    print(city)
# We need to create Iter Protocol in order not to create Iterator Object all the time
# So now our Cities class is iterable!!!
class Cities:
    def __init__(self):
        self._cities = ['Paris', 'Berlin','New York', 'London']
        self._index = 0
    
    def __len__(self):
        return len(self._cities)
    
    def __iter__(self):
        return CityIterator(self)
cities = Cities()
for city in cities:
    print(city)
for city in cities:
    print(city)
list(enumerate(cities))
sorted(cities,key=lambda x: len(x))
# MAKING Cities into sequence class
class Cities:
    def __init__(self):
        self._cities = ['Paris', 'Berlin','New York', 'London']
        self._index = 0
    
    def __len__(self):
        return len(self._cities)
    
    def __iter__(self):
        return CityIterator(self)
    
    def __getitem__(self,s):
        print('getting item...')
        return self._cities[s]
    
class CityIterator:
    def __init__(self, city_obj):
        self._city_obj = city_obj
        self._index = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self._index >= len(self._city_obj):
            raise StopIteration
        else:
            item = self._city_obj._cities[self._index]
            self._index += 1
            return item
cities = Cities()
cities[0]
# It will use iterator __iter__ method if it is available if not it will use __getitem__
for city in cities:
    print(city)
l = [1,2,3,4]
iter(l)
l.__iter__
l.__getitem__
l_iter = iter(l)
for i in l_iter:
    print(i)
# Our iter is exhaused now
for i in l_iter:
    print(i)
# If we run __next__ :::
next(l_iter)<jupyter_output><empty_output><jupyter_text>Consuming Iterators Manually<jupyter_code>s = 'I sleep all night'
iter_s = iter(s)
iter_s
# Only can iterate on iterator not iterable
next(iter_s)
#OR
iter_s.__next__()
next(iter_s)
for i in iter_s:
    print(i,end=' ')
#After looping through iterable using iterator, it is exhaused by StopIteration exception
with open('cars.csv') as file:
    for line in file:
        print(line,end='')
with open('cars.csv') as file:
    row_index = 0
    for line in file:
        if row_index == 0:
            # header row
            headers = line.strip('\n').split(',')
            print(headers)
        elif row_index == 1:
            #data type row
            data_types = line.strip('\n').split(',')
            print(data_types)
        else:
            #data row
            data = line.strip('\n').split(',')
            print(data)
        row_index += 1
        
from collections import namedtuple
cars = []
with open('cars.csv') as file:
    row_index = 0
    for line in file:
        if row_index == 0:
            # header row
            headers = line.strip('\n').split(',')
            Car = namedtuple('Car', headers)
        elif row_index == 1:
            #data type row
            data_types = line.strip('\n').split(',')
            print(data_types)
        else:
            #data row
            data = line.strip('\n').split(',')
            car = Car(*data)
            cars.append(car)
            print(data)
        row_index += 1
cars
def cast(data_type,value):
    if data_type == 'STRING':
        return str(value)
    elif data_type == 'INT':
        return int(value)
    else:
        return bool(value)
data_types = ['STRING', 'INT', 'STRING', 'BOOL']

def cast_row(data_types,data_row):
    return [cast(data_type,value) for data_type, value in zip(data_types,data_row)]
from collections import namedtuple
cars = []
with open('cars.csv') as file:
    row_index = 0
    for line in file:
        if row_index == 0:
            # header row
            headers = line.strip('\n').split(',')
            Car = namedtuple('Car', headers)
        elif row_index == 1:
            #data type row
            data_types = line.strip('\n').split(',')
            print(data_types)
        else:
            #data row
            data = line.strip('\n').split(',')
            data = cast_row(data_types,data)
            car = Car(*data)
            cars.append(car)
            print(data)
        row_index += 1
cars
# SHORTER WAY
cars1 = []

with open('cars.csv') as file:
    file_iter = iter(file)
    headers = next(file_iter).strip('\n').split(',')
    Car = namedtuple('Car',headers)
    data_types = next(file_iter).strip('\n').split(',')
    for line in file_iter:
        data = line.strip('\n').split(',')
        data = cast_row(data_types,data)
        car = Car(*data)
        cars.append(car)
cars
#EVEN SHORTER USING LIST COMPREHENSION
with open('cars.csv') as file:
    file_iter = iter(file)
    headers = next(file_iter).strip('\n').split(',')
    Car = namedtuple('Car',headers)
    data_types = next(file_iter).strip('\n').split(',')
    
    cars = [Car(*cast_row(data_types,line.strip('\n').split(','))) for line in file_iter]
cars<jupyter_output><empty_output><jupyter_text>Cyclic Iterators ```1234567NSWE1N 2S 3W 4E 5N 6S ...```<jupyter_code>class CyclicIterator:
    def __init__(self,lst):
        self.lst = lst
        self.i = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        result = self.lst[self.i%len(self.lst)]
        self.i += 1
        return result
iter_cycl = CyclicIterator('NSWE')
for _ in range(10):
    print(next(iter_cycl))
iter_cycl1 = CyclicIterator([10,20,30,40])
for _ in range(10):
    print(next(iter_cycl1))
numbers = range(10)
iter_cycl = CyclicIterator('NSWE')
list(zip(list(numbers),iter_cycl))
n = 10
iter_cycl = CyclicIterator('NSWE')
for i in range(1,n+1):
    direction = next(iter_cycl)
    print(f'{i}{direction}')
n = 10
iter_cycl = CyclicIterator('NSWE')
items = [str(i) + next(iter_cycl) for i in range(1,n+1)]
items
n = 10
iter_cycl = CyclicIterator('NSWE')
items = [str(index)+string for index,string in zip(range(1,n+1),iter_cycl)]
items
import itertools
n = 10
iter_cycl = CyclicIterator('NSWE')
items = [f'{i}{next(iter_cycl)}' for i in range(1,n+1)]
items
n = 10
iter_cycl = itertools.cycle('NSWE')
items = [f'{i}{next(iter_cycl)}' for i in range(1,n+1)]
items
help(itertools.cycle)
s = {'a','x','X',100}
list(s)
class CyclicIterator:
    def __init__(self,iterable):
        self.iterable = iterable
        self.iterator = iter(self.iterable)

    def __iter__(self):
        return self
    
    def __next__(self):
        try:
            item = next(self.iterator)
        except StopIteration:
            self.iterator = iter(self.iterable)
            item = next(self.iterator)
        finally:
            return item
iter_cycl = CyclicIterator('abc')
for i in range(5):
    print(i,next(iter_cycl))<jupyter_output>0 a
1 b
2 c
3 a
4 b<jupyter_text>Lazy Iterables<jupyter_code>import math
class Circle:
    def __init__(self,r):
        self.radius = r
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self,r):
        self._radius = r
        self.area = math.pi*(r**2)
c = Circle(1)
c.radius
c.area
class Circle:
    def __init__(self,r):
        self.radius = r
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self,r):
        self._radius = r
    
    @property
    def area(self):
        return math.pi*(self.radius**2)
c = Circle(1)
c.area
class Circle:
    def __init__(self,r):
        self.radius = r
        self._area = None
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self,r):
        self._radius = r
        self._area = None
    
    @property
    def area(self):
        if self._area is None:
            print('Calculating...')
            self._area = math.pi*(self.radius**2)
        return self._area
c = Circle(5)
c.area
c.area
c.radius = 4
c.area
class Factorials:
    def __init__(self,length):
        self.length = length
        
    def __iter__(self):
        return self.FactIter(self.length)
        
    class FactIter:
        def __init__(self,length):
            self.length = length
            self.i = 0
        
        def __iter__(self):
            return self
        
        def __next__(self):
            if self.i >= self.length:
                raise StopIteration
            else:
                result = math.factorial(self.i)
                self.i += 1
                return result
facts = Factorials(5)
list(facts)
class Factorials:
    def __iter__(self):
        return self.FactIter()
        
    class FactIter:
        def __init__(self):
            self.i = 0
        
        def __iter__(self):
            return self
        
        def __next__(self):
            result = math.factorial(self.i)
            self.i += 1
            return result
facts = Factorials()
fact_iter = iter(facts)
next(fact_iter)
next(fact_iter)
next(fact_iter)
next(fact_iter)
next(fact_iter)<jupyter_output><empty_output><jupyter_text>Python's Built-in Iterables and Iterators<jupyter_code>r = range(10)
'__next__' in dir(r)
'__iter__' in dir(r)
z = zip([1,2,3],'abc')
'__next__' in dir(z)
list(z)
list(z)
f =  open('cars.csv')
print(next(f))
print(f.__next__())
print(f.readline())
f.close()
with open('cars.csv') as f:
    for row in f:
        print(row,end='')
with open('cars.csv') as f:
    print(type(f))
    print('__next__' in dir(f))
with open('cars.csv') as f:
    print(iter(f) is f)
with open('cars.csv') as f:
    l = f.readlines() # <----------- not the best because it will load all file to the memory
origins = set()
with open('cars.csv') as f:
    rows = f.readlines()
for row in rows[2:]:
    origin = row.strip('\n').split(',')[-2]
    origins.add(origin)
print(origins)
origin = set()
with open('cars.csv') as f:
    next(f),next(f)
    for row in f:
        origin = row.strip('\n').split(',')[-2]
        origins.add(origin)
print(origins)
e = enumerate('Python rocks')
iter(e) is e
'__next__' in dir(e)
list(e)
list(e) # <------------ got exhausted<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.132796940970286, 'fraction_numerical': 0.0121883215167689, 'mean_word_length': 2.4188453159041394, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '202016', 'license': 'MIT', 'n_tokens_mistral': 4344, 'n_tokens_neox': 4212, 'n_words': 1038, 'path': 'Iterating Collections.ipynb', 'repo_name': 'nikgun1984/Python-Programming-Part-2', 'size': '12553'}