<jupyter_start><jupyter_text>Variable scope - LEGB- Local- Enclosing- Global- Built-in Local vs Global variable<jupyter_code>team = "local value"

def change_value():
    global team

    team = 'making global'
print(team)

change_value()

print(team)
import builtins
dir(builtins)
def power(p):
    def inner(n):
        return n**p
    return inner

sq = power(2)
cb = power(3)

print(sq(5))
print(cb(5))
#Enclosing - In nested function, 'nonlocal' can be used to change a variable like 'global'

def outer():
    x = 1

    def inner():
        # nonlocal x
        x = 5
        print(x)
    
    inner()
    print(x)

outer()
def outer():
    x = 1

    def inner():
        nonlocal x
        x = 5
        print(x)
    
    inner()
    print(x)

outer()
#Flexible functions
# *args - argument

def adding(*args):
    ad = 0

    for i in args:
        ad += i
    return ad 

adding(3,4,5)
adding(4,3,1,6)
# **kwargs - keyword arguments in key-value pairs

def printing(**kwargs):

    for k,v in kwargs.items():
        print(k,'-',v)

printing(name='abc',mobile='0000')
printing(a = 'a', b = 'b',c = 'c', d = 'd')
#Lambda function
a = [1,2,3,4]
list(map(lambda x: x**2,a))
check = lambda x: x**2

check(3)
mul = (lambda a,b: a*b)

mul(3,4)
a = [1,2,3,4,5,6,7,8,9,10]

list(map(lambda x: x%2 == 0,a))  #mapping
list(filter(lambda x: x%2 == 0,a))   #filtering
def even_num(n):
    if n%2 == 0:
        return True

l = list(range(1,11))
filter(even_num,l)
list(filter(even_num,l))
#Reduce - It returns a single value as a result

from functools import reduce
a = list('Hello, World!')

reduce(lambda x,y: x+y,a)  #reducing
reduce(lambda x,y: x+y,a) == ''.join(a)<jupyter_output><empty_output><jupyter_text>A function to print ASCII value<jupyter_code>ord('a'),ord('A')
ord('/')
ord('\n')<jupyter_output><empty_output><jupyter_text>Iterator & Iterable- Iterable is an object, which one can iterate over. ... Iterator is an object, which is used to iterate over an iterable object using __next__() method.- https://www.geeksforgeeks.org/python-difference-iterable-iterator/- Datacamp     - iterable is an object that can return an iterator    -  iterator is an object that keeps state and produces the next value when you call next() on it---- `iter()` - used to build iterator objects - `next()` - used to iterate over the content- raises `StopIteration` error when iteration is complete<jupyter_code>a = 'Hey'  
b = iter(a)

next(b) #iterating element-wise
next(b)
next(b)
next(b)
a = 'Hey'
b = iter(a)

print(*b)  #iterating all together
#iterating over a file

f = open('abc.txt')
b = iter(f)

next(b)  #printing first line
next(b)  #printing second line and so on ...
class RangeFunction:
    def __init__(self,start,end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.start < self.end:
            x,self.start = self.start,self.start + 1
            return x
        else:
            raise StopIteration
itr = RangeFunction(0,5)
print(itr.__next__())
print(itr.__next__())
print(itr.__next__())
print(itr.__next__())
print(itr.__next__())

print(itr.__next__())
for i in RangeFunction(0,5):
    print(i)<jupyter_output>0
1
2
3
4<jupyter_text>Enumerate<jupyter_code>for index,value in enumerate(list('hello')): print('Index :',index,'&','Value :',value)
a = list('hello')
b = enumerate(a)
list(b)
a = list('hello')
b = enumerate(a,start=2)   #starting index from 2 rather than default 0
list(b)<jupyter_output><empty_output><jupyter_text>zip<jupyter_code>a = list('hello')
b = list(range(1,6))

c = zip(a,b)  #zipping together
print(c)
print(*c)
c = zip(a,b)
list(c)
a = list('hello')  #5
b = list(range(6)) #6
c = list('abcdefg') #7

for i,j,k in zip(a,b,c):
    print(i,j,k) #only prints 5
a = tuple('hello')  
b = tuple(range(5))

z1 = zip(a,b)
print(*z1)

z1 = zip(a,b)
r1, r2 = zip(*z1)

print(r1 == a)
print(r2 == b)
r1
a = tuple('hello')  
b = tuple(range(5))

print(zip(a,b))
# print(list(zip(a,b)))
print(dict(zip(a,b)))
#working with large data > memory capacity
import pandas as pd

df = pd.DataFrame({'a':list(range(1,11)),'b':list(range(11,21))})
df.to_csv('df.csv',)   #try chunksize with to_csv
df
for chunk in pd.read_csv('df.csv',chunksize=5):
    print(chunk) #do computation of each chunk and then current chunk will gets replaced with another
#creating an iterable reader object and one can use next()
temp = pd.read_csv('df.csv',chunksize=5)
next(temp)
next(temp)<jupyter_output><empty_output><jupyter_text>Comprehensions<jupyter_code>[n**3 if n%3 == 0 else '?' for n in range(10)]
type([n**3 if n%3 == 0 else '?' for n in range(10)])
{i: i**2 for i in range(10)}
type({i: i**2 for i in range(10)})<jupyter_output><empty_output><jupyter_text>Generators- It is used to implement iterators. - use `()` instead of `[]` to produce generator - It is like a list but it **does not** store list in **memory**.  - It can be used to iterate over a large number like 100 ^ 1000000 without storing the list in memory.- Generator function uses `yield` for iteration and unlike `return`(which terminates after returning value), it returns a value and pauses the execution while maintaining the internal states and get executed when `next()` is called until hitting `StopIteration` like error.- Generator Function defined like a regular function(def) and yields a sequence of values instead of returning a single value - datacampReferences: - https://www.tutorialsteacher.com/python/python-generator- Datacamp- http://www.wellho.net/mouth/561_Python-s-Generator-functions.html<jupyter_code>def MyRange(start,end):
    while start < end:
        yield start
        start += 1

a = MyRange(0,5)

print(next(a))
print(next(a))
print(next(a))
print(next(a))
print(next(a))
for i in MyRange(0,5):
    print(i)
(n**3 if n%3 == 0 else '?' for n in range(10))
temp = (n**3 if n%3 == 0 else '?' for n in range(10))
next(temp)  #lazy evaluation  | working because it is a generator but couldn't if would be a list
next(temp)
a = [n**3 if n%3 == 0 else '?' for n in range(10)]
next(a)  #do not work because it is not a generator
[num for num in range(100**1000000)]  #this will not work because of high computational requirement
(num for num in range(100**1000000))  # this will easily work, because it is generator
list((i for i in range(10)))
#generator function
def numbers(n):
    i = 0
    while i<n:
        yield i
        i += 1

temp = numbers(5)
next(temp)
next(temp)
for i in temp: print(i)
import math
def factors(n):
    for i in range(1,int(math.sqrt(n))+1):
        if n%i == 0:
            yield i   #function stops execution when return condition met, but generator returs both yield values
            yield n//i

            
for i in factors(8):
    print(i)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.14482758620689656, 'fraction_numerical': 0.02450476889214967, 'mean_word_length': 3.8, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 29, 'https://': 2, 'lorem ipsum': 0, 'www.': 3, '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': '401627', 'license': 'MIT', 'n_tokens_mistral': 2529, 'n_tokens_neox': 2432, 'n_words': 769, 'path': '01 - Basics/06-Generator_Map_reduce_filter_pdChunk.ipynb', 'repo_name': 'yesdeepakmittal/PY_training', 'size': '6815'}