# function to greet people
def greet(name):
return f'Hi, {name}'
# storing in a variable
my_var = greet
print(my_var('Kanan'))
my_var('Jack')
from math import sqrt
# we will use first 2 functions as an argument
def sq_root(num):
return sqrt(num)
def cube (num):
return num ** 3
# after finding the result, functions uses arguments to give meaningful answer
def meaningful_func(func, num, message):
res = func(num)
return f'{message} of the {num} is the {res:.2f}'
print(meaningful_func(sq_root, 20, 'square root'))
print(meaningful_func(cube, 5 , 'cube'))
from math import sqrt
# we will create 2 lambda functions and store them into a dictionary with boolean keys
bool_list = [True, False]
func1 = lambda x: x ** 2
func2 = lambda x: x ** 3
func_list = [func1, func2]
dic = dict(zip(bool_list, func_list))
# here we will use stored functions, if key is True
num = 10
for key, value in dic.items():
if key:
print(value(num))
# let's first look at the function with pre-defined number of arguments
def add(a, b):
return a + b
print(add(1, 2, 3))
def add(*args):
print(args)
return (sum(args))
print(add(1, 2, 3))
add(1, 2, 3, 4, 5)
# we can use non-keyword arguments after some predefined arguments
def func(first_argument, *args):
print(f'{first_argument} is a predefined argument')
print('Non-keyword arguments:')
for i in args:
print(i, end = ' ')
print(func('Python', 'Java', 'Julia', 'C++'))
def func(**kwargs):
for key, value in kwargs.items():
print(f'{value} has been stored at {key}')
print(func(argument1 = 'Python', argument2 = 'Java'))
# args and kwargs together
def func(*args, **kwargs):
print('Non-keyword arguments:')
for i in args:
print(i)
print('Keyword arguments:')
for key, value in kwargs.items():
print(f'{key}:{value}')
print(func(65, 80, 90, grade4=95, grade5=100))
# let's create a variable and assign a value to it
my_var = 15
print(None)
# let's see what happens when we overwrite the built-in function
print = lambda x: f'hello, {x}'
print('Kanan')
print('Python', 'Java')
# here is the list of the all built-in names predefined in Python
dir(__builtins__)
# let's delete the custom print function and continue
del print
print('Python', 'Java')
for i in range(5):
a = 3
print(a)
# creating a local variable
def add(a, b):
c = a + b
return c
add(2, 3)
c
a = 5
def multiply_a(x):
return a * x
print(multiply_a(4))
a = 5
b = 3
def multiply_a(x):
b = a * x
return b
print(multiply_a(4))
print(b)
a = 5
def func():
global a
a = 20
func()
print(a)
a = 10
def my_func():
print(a)
a = 3
return a
my_func()
# accessing to global variable from inner local scope
a = 10
def outer():
def inner():
print(a)
inner()
outer()
# accessing to the nonlocal scope from the inner local scope
def outer():
a = 10
def inner():
print(a)
inner()
outer()
# Modifying global variable from enclosing scope
a = 10
def outer():
global a
a = 5
def inner():
print(a)
inner()
print(outer())
print(a)
# Modifying the global variable from inner local scope
a = 10
def outer():
def inner():
global a
a = 3
inner()
print(a)
print(outer())
print(a)
# let's try to change value of nonlocal variable from inner local scope
def outer():
a = 10
def inner():
a = 3
print(a)
inner()
print(a)
print(outer())
# Modifiying nonlocal variable
def outer():
a = 10
def inner():
nonlocal a
print(a)
a = 3
print(a)
inner()
print(a)
print(outer())
def outer():
a = 10
def inner():
print(a)
a = 3
inner()
print(a)
print(outer())
# modifying the global variable from outer function's scope
a = 10
def outer():
global a
a = 5
def inner1():
def inner2():
print(a)
inner2()
inner1()
print(outer())
print(a)
# modifiying the global variable from first nested scope
a = 10
def outer():
def inner1():
global a
a = 5
def inner2():
print(a)
inner2()
inner1()
print(outer())
print(a)
# modifying the global variable fron the second nested scope
a = 10
def outer():
def inner1():
def inner2():
global a
a = 5
print(a)
inner2()
inner1()
print(outer())
print(a)
# modifying nonlocal variable in outer function's scope from the first nested scope
def outer():
a = 10
def inner1():
nonlocal a
a = 5
def inner2():
print(a)
inner2()
inner1()
print(a)
print(outer())
# We can do the same from the second nested scope
def outer():
a = 10
def inner1():
def inner2():
nonlocal a
a = 2
print(a)
inner2()
inner1()
print(a)
print(outer())
# first we should be aware of using nonlocal keyword
# nonlocal means Python will only look at local scopes, not global
a = 10
def outer():
def inner1():
def inner2():
nonlocal a
a = 3
print(a)
inner2()
inner1()
print(outer())
# having the same variable in both outer and inner local scopes
def outer():
a = 'Python'
def inner1():
a = 'Java'
def inner2():
nonlocal a
a = 'C++'
print('Before inner2:', a)
inner2()
print('After inner2:', a)
inner1()
print('Outer a:', a)
print(outer())
# using the nonlocal keyword in both nested scopes
def outer():
a = 'Python'
def inner1():
nonlocal a
a = 'Java'
def inner2():
nonlocal a
a = 'C++'
print('Before inners:', a)
inner2()
print('After inner2:', a)
inner1()
print('outer:', a)
print(outer())
# what if we also had a global variable with the same name?
a = 'Python'
def outer():
a = 'Java'
def inner1():
nonlocal a
a = 'C++'
def inner2():
global a
a = 'Julia'
print('Before inner2:', a)
inner2()
print('After inner2:', a)
inner1()
print('outer:', a)
print(outer())
print('global:', a)
# a simple example of a closure
def outer():
a = 10
def inner():
print(a)
return inner
func = outer()
print(func())
# let's just write previous code to keep it simple
def outer():
a = 10
def inner():
print(a)
return inner
func = outer()
print(func())
# looking at our free variables and closure
def outer():
a = 10
x = 3
def inner():
x = 5
print(a)
return inner
func = outer()
# looking at the free variables
print(func.__code__.co_freevars)
# looing at the closure
print(func.__closure__)
# let's look at the memory adress of the free variable inside the local scopes
def outer():
a = 10
x = 3
print(hex(id(a)))
def inner():
x = 5
print(hex(id(a)))
print(a)
return inner
func = outer()
print(func())
# modifying our free variable
def outer():
c = 0
def counter():
nonlocal c
c += 1
return(c)
return counter
func1 = outer()
# since our free variable is nonlocal, each time we call the func
# we will modify its current value, i.e add 1 to it
print(func1())
print(func1())
# now let's create a new closure
func2 = outer()
print(func2())
print(func2())
print(func1.__closure__)
print(func2.__closure__)
# creating a shared scope inside outer function
def outer():
c = 0
def counter1():
nonlocal c
c += 1
return c
def counter2():
nonlocal c
c += 1
return c
return counter1, counter2
func1, func2 = outer()
# let's first look at the closures of both functions
# and see the cell memory and free variable memory
print(func1.__closure__)
print(func2.__closure__)
# let's call the func1 several times
print(func1())
print(func1())
# let's now look at the memory address of the free variable
print(func1.__closure__)
print(func2.__closure__)
print(func2())
print(func2())
# we can look at the memory addresses again
print(func1.__closure__)
print(func2.__closure__)
def outer(n):
def inner(x):
return n + x
return inner
func1 = outer(1)
print(func1.__code__.co_freevars)
# creating 2 more closures with different arguments
func2 = outer(2)
func3 = outer(3)
print(func1(5))
print(func2(5))
print(func3(5))
ls = []
for i in range(1, 9):
ls.append(lambda x: x + i)
# let's use the closures
print(ls[0](5))
print(ls[1](5))
print(ls[7](5))
def outer(n):
# inner1 + free variable n is a closure
def inner1(x):
current = x
# inner2 + free variables current and n is a closure
def inner2():
nonlocal current
current += n
return current
return inner2
return inner1
inner1 = outer(10)
print(inner1.__code__.co_freevars)
inner2 = inner1(7)
print(inner2.__code__.co_freevars)
print(inner2())
print(inner2())
# creating a simple decorator that counts how many times a specific function called
def counter(fn):
c = 0
def wrapper(*args, **kwargs):
nonlocal c
c += 1
print(f'{fn.__name__} function is called {c} times.')
return fn(*args, **kwargs)
return wrapper
# let's create some functions to use them with decorator
def mult(a, b):
return a * b
def add(a, b, c):
return a + b + c
mult = counter(mult)
add = counter(add)
print(mult(2, 3))
print(mult(3, 4))
print(add(1, 2, 3))
print(add(2, 3, 4))
print(mult(2, 7))
print(mult.__closure__)
print(mult.__code__.co_freevars)
print(add.__closure__)
print(add.__code__.co_freevars)
# since mult and add functions are already decorated we should define them again
# otherwise we will decorate them twice
@counter
def mult(a, b):
return a * b
@counter
def add(a, b, c):
return a + b + c
# now we can use them as we did before
print(mult(5, 4))
print(mult(3, 4))
print(add(3, 6, 8))
print(add(4, 1, 0))
print(mult(5, 3))
# we can again look at the closures and free variables
print(mult.__closure__)
print(mult.__code__.co_freevars)
print(add.__closure__)
print(add.__code__.co_freevars)
# timer decorator also shows which fibanocci number we are calculating at that time
# it will be helpful in recursive fibonacci function
def timer(fn):
from time import perf_counter
def wrapper(*args, **kwargs):
start = perf_counter()
result = fn(*args, **kwargs)
end = perf_counter()
print(f'finding {args[0]}th fibonacci number')
print(f'{fn.__name__} function took {end - start}s to run.\n')
return result
return wrapper
# Let's write 2 fifferent functions to find fibonacci numbers, one with the recursion, other one with the loop.
# Here is the Fibonacci series if you don't know: 1, 1, 2, 3, 5, 8, 13, 21 ... .
# First 2 fibonacci numbers are 1 and following fibonacci numbers are sum of the previos fibonacci numbers.
#
# recursion
@timer
def fib_rec(n):
if n <= 2:
return 1
return fib_rec(n-1) + fib_rec(n-2)
print(fib_rec(6))
def fib_rec(n):
if n <= 2:
return 1
return fib_rec(n-1) + fib_rec(n-2)
@timer
def fib_rec_helper(n):
return fib_rec(n)
print(fib_rec_helper(5))
print(fib_rec_helper(35))
# for loop
@timer
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
print(fib_loop(5))
print(fib_loop(35))
# let's write counter decorator again
def counter(fn):
c = 0
def wrapper(*args, **kwargs):
nonlocal c
c += 1
print(f'{fn.__name__} function called {c} times.')
return fn(*args, **kwargs)
return wrapper
@counter
def greet(name):
"""
this function greets people.
"""
return f'Hi, {name}!'
print(greet('Kanan'))
print(help(greet))
# writing decorator with hardcoded function name and docstring
def counter(fn):
c = 0
def wrapper(*args, **kwargs):
nonlocal c
c += 1
print(f'{fn.__name__} function called {c} times.')
return fn(*args, **kwargs)
# overwriting the metadat of the inner function
wrapper.__name__ = fn.__name__
wrapper.__doc__ = fn.__doc__
return wrapper
# we also need to write our function again as we had decorated it previously
# this time, let's also add a signature(showing data types of the objects) to our function
@counter
def greet(name:str) -> str:
'''
This function greets people.
'''
return f'Hi, {name}!'
print(greet('Kanan'))
print(help(greet))
# using wrap function manually
# we import wrap from inside of the decorator
# so that whenever the decorator used it will find wrap from enclosing scope
def counter(fn):
c = 0
from functools import wraps
def wrapper(*args, **kwargs):
nonlocal c
c += 1
print(f'{fn.__name__} function called {c} times.')
return fn(*args, **kwargs)
wrapper = wraps(fn)(wrapper)
return wrapper
@counter
def greet(name:str) -> str:
'''
This function greets people.
'''
return f'Hi, {name}!'
print(greet('Kanan'))
print(help(greet))
# let's do the same thing using @
def counter(fn):
c = 0
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
nonlocal c
c += 1
print(f'{fn.__name__} function called {c} times.')
return fn(*args, **kwargs)
return wrapper
@counter
def greet(name:str) -> str:
'''
This function greets people.
'''
return f'Hi, {name}!'
print(greet('Kanan'))
print(help(greet))
def date_time(fn):
from datetime import datetime
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
print(f'{fn.__name__} function run on: {datetime.today().strftime("%Y-%m-%d %H:%M:%S")}')
return fn(*args, **kwargs)
return wrapper
def add(a:int, b:int)->int:
return a + b
# applying time decorator
date_time = date_time(add)
# applying counter decorator
counter = counter(date_time)
# result
print(counter(2, 3))
# let's copy and paste our decorators
def counter(fn):
c = 0
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
nonlocal c
c += 1
print(f'{fn.__name__} function called {c} times.')
return fn(*args, **kwargs)
return wrapper
def date_time(fn):
from datetime import datetime
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
print(f'{fn.__name__} function run on: {datetime.today().strftime("%Y-%m-%d %H:%M:%S")}')
return fn(*args, **kwargs)
return wrapper
@counter
@date_time
def add(a:int, b:int)->int:
return a + b
print(add(2, 3))
# we can also check that after the double decorating, function keeps its metadata
print(help(add))
def timer(fn):
from time import perf_counter
from functools import wraps
# we will now create pass our fn function to wraps to create a decorator
dec = wraps(fn)
@dec
def wrapper(*args, **kwargs):
start = perf_counter()
result = fn(*args, **kwargs)
end = perf_counter()
print(f'{fn.__name__} function took {end - start}s to run.\n')
return result
return wrapper
# writing fibonacci number finder function again
@timer
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
print(fib_loop(10))
print(help(fib_loop))
# timer decorator with additional parameter
def timer(fn, n):
from time import perf_counter
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
total = 0
for _ in range(n):
start = perf_counter()
result = fn(*args, **kwargs)
end = perf_counter()
total += end - start
print(f'{fn.__name__} function took {total / n}s to run.\n')
return result
return wrapper
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
# now we can create our closure by passing 2 parameters to time decorator
dec = timer(fib_loop, 15)
print(dec(10))
def timer(n):
def dec(fn):
from time import perf_counter
from functools import wraps
@wraps(fn)
def wrapper(*args, **kwargs):
total = 0
for _ in range(n):
start = perf_counter()
result = fn(*args, **kwargs)
end = perf_counter()
total += end - start
print(f'{fn.__name__} function took {total / n}s to run.\n')
return result
return wrapper
return dec
# Now we can use parametrized timer decorator both manually and by using the @ sign.
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
# we pass the parameter to create an actual decorator
dec = timer(15)
fib_loop = dec(fib_loop)
print(fib_loop(5))
# using @ sign
@timer(15)
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
print(fib_loop(5))
# Passing the function that will be decorated during the initialization(non-parametrized class decorator)
class decorator:
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
print('Function decorated.')
return self.fn(*args, **kwargs)
def add(a, b, c=9):
return a + b + c
# manual decorating
# first we need to create our object by passing our function to the class
obj = decorator(add)
print(obj(1, 2))
# using @ sign
@decorator
def add(a, b, c=9):
return a + b + c
print(add(1, 2))
# let's write our parametrized timer decorator as parametrized class decorator
class class_timer:
def __init__(self, n):
self.n = n
def __call__(self, fn):
from time import perf_counter
def wrapper(*args, **kwargs):
total = 0
for _ in range(self.n):
start = perf_counter()
result = fn(*args, **kwargs)
end = perf_counter()
total += end - start
print(f'{fn.__name__} function took {total / self.n}s to run.')
return result
return wrapper
# manually using the parametrized timer class decorator on fibinacci number finder function
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
# first we need to create a callable object by passing a parameter
# this parameter represents how many times fib_loop function will run, then we pass our function to decorate
obj = class_timer(10)
fib_loop = obj(fib_loop)
print(fib_loop(5))
# using @ sign
# we will use object of a class as a decorator
@class_timer(10)
def fib_loop(n):
prev = 1
curr = 1
for i in range(n-2):
prev, curr = curr, prev + curr
return curr
print(fib_loop(5))