Thursday, February 29, 2024

Slice Object in Python

Built-in type that represents a range of indices. It's commonly used with sequences (like lists, strings, and tuples) to extract portions of the sequence.


list_of_students = [(1,"Joe"), (2,"John"), (3,"Jacob"), (4,"Jack"),
 (5,"Karan"), (6, "Kavya"), (7,"Kamal")]

odd_slice = slice(0,len(list_of_students),2)
even_slice = slice(1,len(list_of_students),2)
list_of_group_one = list_of_students[odd_slice]
list_of_group_two = list_of_students[even_slice]
print(list_of_group_one)
print(list_of_group_two)

Output:

[(1, 'Joe'), (3, 'Jacob'), (5, 'Karan'), (7, 'Kamal')]
[(2, 'John'), (4, 'Jack'), (6, 'Kavya')]

Wednesday, February 28, 2024

Python Decorator

 In Python, a decorator is a design pattern that allows you to extend or modify the behavior of functions or methods without modifying their actual code. Decorators are applied using the @decorator syntax and are a concise way to wrap a function or method with additional functionality.

It helps you to off/on some functionality.


def log_function_call(func):
def wrapper(*args):
print(f"Calling {func.__name__} with arguments {args}")
result = func(*args)
print(f"{func.__name__} execution completed.")
return result
return wrapper

@log_function_call
def add_numbers(a, b):
return a + b

@log_function_call
def multiply_numbers(x, y):
return x * y

# Calling decorated functions
result_add = add_numbers(3, 5)
print(result_add)
result_multiply = multiply_numbers(
4, 6)
print(result_multiply)

Output:
Calling add_numbers with arguments (3, 5)
add_numbers execution completed.
8
Calling multiply_numbers with arguments (4, 6)
multiply_numbers execution completed.
24

Now if we comment out the decorator,
def log_function_call(func):
def wrapper(*args):
print(f"Calling {func.__name__} with arguments {args}")
result = func(*args)
print(f"{func.__name__} execution completed.")
return result
return wrapper

# @log_function_call
def add_numbers(a, b):
return a + b

# @log_function_call
def multiply_numbers(x, y):
return x * y

# Calling decorated functions
result_add = add_numbers(3, 5)
print(result_add)
result_multiply = multiply_numbers(
4, 6)
print(result_multiply)

Output:
8
24

Slice vs Split

Slicing is a broader concept applicable to sequences like lists, tuples, and strings. It involves extracting a portion, or a "slice," of a sequence based on indices. It is not an in-built function, it is a concept.

 art_board = "ArtBoard1"

list_of_shapes = ["Square", "Triangle", "Circle"]
tuple_of_colors = ('59D5E0', 'F5DD61', 'FAA300', 'F4538A')
# Slice works on both string and Iterables
# Get first 3 characters
print(art_board[:3])
# Reverse the list
print(list_of_shapes[::-1])
print(tuple_of_colors[1::2])


Output:
Art
['Circle', 'Triangle', 'Square']
('F5DD61', 'F4538A')

Split is used to divide a string into a list of substrings based
on a specified delimiter. 
By default, the delimiter is a space. 
# Split only works on String and create a list as outcome
my_name = "Priyanka Chakraborti"
my_first_name = my_name.split()[0]
my_last_name = my_name.split()[
1]
print(my_first_name, my_last_name, sep=',')
print(my_name.split())
# Change list to Tuple
print(tuple(my_name.split()))
Output:
Priyanka,Chakraborti
['Priyanka', 'Chakraborti']
('Priyanka', 'Chakraborti')

Understanding the Purpose of if name == 'main' in Python Scripts

 The __name__ == "__main__" construct in Python is a common pattern used to determine whether a Python script is being run as the main program or if it is being imported as a module into another script. This check is often used to control the execution of code that should only run when the script is executed directly.


We have two .py files.

File 1: sample.py


print('This is sample.py code')


def print_sample_text():
print('This is a sample text')


# __name__ is in-built variable
if __name__ == "__main__":
print('Sample.py is executed directly')
else:
print('Sample.py is imported')
print(f"Its name is: {__name__}")

File 2: demo.py
import sample
sample.print_sample_text()

if __name__ == "__main__":
print('demo.py is executed directly')
else:
print('demo.py is imported')
print(f"Its name is: {__name__}")

Now, if we run sample.py, output will be like:

This is sample.py code
Sample.py is executed directly

If we run demo.py, output will be like:
This is sample.py code
Sample.py is imported
Its name is: sample
This is a sample text
demo.py is executed directly

Now, notice the highlighted line. It should be executed 
only when we are running sample.py. But it is getting executed 
while import. Lets move this to the if block.

# print('This is sample.py code')


def print_sample_text():
print('This is a sample text')


# __name__ is in-built variable
if __name__ == "__main__":
print('Sample.py is executed directly')
print('This is sample.py code')
else:
print('Sample.py is imported')
print(f"Its name is: {__name__}")

Now, the output is:
Sample.py is imported
Its name is: sample
This is a sample text
demo.py is executed directly


Random module and String module

Example 1: 


import random,string


list_of_alphabets = [letter
for letter in string.ascii_letters]
random.shuffle(list_of_alphabets)
print(list_of_alphabets)
Output:
['Y', 'M', 'b', 'F', 'X', 'r', 'm', 'J', 'w', 'o', 
'Z', 'U', 'g', 'l', 'c', 'f', 'A', 'B', 'N', 'G', 'L', 'V', 'h', 
'n', 'I', 'T', 'x', 'y', 'E', 'H', 'D', 'q', 'd', 'O', 't', 'R', 
'S', 's', 'v', 'P', 'e', 'Q', 'k', 'p', 'i', 'W', 'K', 'C', 'j', 'z',
 'a', 'u']

Example 2:

import random
import string


def generate_random_password(length=12):
if length < 8 or length > 16:
raise ValueError("Password length must be between 8 and 16 characters")

password = []

# Combine letters and symbols
list_of_chars = [letter for letter in string.ascii_letters]
list_of_symbols = [symbol
for symbol in string.punctuation]
list_of_chars.extend(list_of_symbols)

# Shuffle the characters
random.shuffle(list_of_chars)

# Select characters for the password
for _ in range(length):
password.append(
random.choice(list_of_chars))

return ''.join(password)


# Generate a random password of length between 8 and 16
random_password = generate_random_password(random.randint(8, 16))
print(random_password)

Output:
x#=K@_&p.D

Access and modify global variable

 # Trying to change global variable

my_surname = 'Chakraborti'


def change_my_surname():
my_surname = 'Chakraborty'


print(f"My Surname before change: {my_surname}")
# Call change my surname function
change_my_surname()
print(f"My Surname after change: {my_surname}")

Output:
My Surname before change: Chakraborti
My Surname after change: Chakraborti

The above code could not change the my_surname (global variable). 
Actually in the change_my_surname() function,
it is creating a local variable.
This can be verified just writing a print statement.

# Access global variable
my_surname = 'Chakraborti'



def change_my_surname():
    my_surname = 'Chakraborty'
print(f"Changing surname to: {my_surname}")


print(f"My Surname before change: {my_surname}")
# Call change my surname function
change_my_surname()
print(f"My Surname after change: {my_surname}")
Output:
My Surname before change: Chakraborti
Changing surname to: Chakraborty
My Surname after change: Chakraborti

However, inside function, we can access the value.
# Access global variable
my_surname = 'Chakraborti'



def change_my_surname():
print(f"Current Surname: {my_surname}")


change_my_surname()

Output:
Current Surname: Chakraborti
Now to change the global variable,

# Change global variable
my_surname = 'Chakraborti'

def change_my_surname():
global my_surname
my_surname =
'Chakraborty'
print(f"Changing surname to: {my_surname}")


print(f"My Surname before change: {my_surname}")
# Call change my surname function
change_my_surname()
print(f"My Surname after change: {my_surname}")
Output:
My Surname before change: Chakraborti
Changing surname to: Chakraborty
My Surname after change: Chakraborty

Combine related information using zip function

Example 1:

 # Example with three lists

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 22]
cities = ["New York", "San Francisco", "Los Angeles"]

# Using zip to combine the three lists
combined = zip(names, ages, cities)

# Converting the result to a list for printing
# Example with three lists
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 22]
cities = ["New York", "San Francisco", "Los Angeles"]

# Using zip to combine the three lists
combined = zip(names, ages, cities)

# Converting the result to a list for printing
result_list = list(combined)

# Output
print(result_list)
Output:
[('Alice', 25, 'New York'), ('Bob', 30, 'San Francisco'), 
('Charlie', 22, 'Los Angeles')]

Modified Example 1:

# Example with three lists
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 22]
cities = ["New York", "San Francisco", "Los Angeles"]

# Using zip to combine the three lists
combined = zip(names, ages, cities)

# Converting the result to a list for printing
# Example with three lists
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 22]
cities = ["New York", "San Francisco", "Los Angeles"]

# Using zip to combine the three lists
combined = zip(names, ages, cities)

for emp in combined:
print(f"Employee name: {emp[0]},
Employee Age: {emp[1]} and Work location is {emp[2]}")

Output:
Employee name: Alice, Employee Age: 25 and Work location is New York
Employee name: Bob, Employee Age: 30 and Work location is San Francisco
Employee name: Charlie, Employee Age: 22 and Work location is Los Angeles

**kwargs to handle any number of keyword arguments

 def my_grocery_list(**kwargs):

    print(kwargs)
print(type(kwargs))
for item in kwargs:
print(f"{kwargs[item]} {item}")

my_grocery_list(toothpaste='Colgate',toothbrush='Colgate',
num_of_toothbrush=2)

Output:

{'toothpaste': 'Colgate', 'toothbrush': 'Colgate',
'num_of_toothbrush': 2}
<class 'dict'> Colgate toothpaste Colgate toothbrush 2 num_of_toothbrush

*args to accept any number of positional arguments

 Consider the below case:

We need to all the sum all the inputs. Number of inputs are unknown.

sum(5,8,9)
The above will not work. sum() takes at most 2 arguments (3 given).

But, sum() can calculate the sum of Iterable i.e. means List, Tuple etc.

sum([5,8,9])
or
sum((5,8,9))
* means any number of arguments.
def sum_of_nums(*args):
print(args) -> o/p: (3, 8, 8, 190)
print(type(args)) -> <class 'tuple'>
return sum(args)


print(sum_of_nums(3, 8, 8, 190))

How to access Nested function from outside?

def func_a():
print('Task triggered for function A')

def func_b():
print('Task triggered for function B')

In this case, we cannot call func__b from outside. We can only access 
func_b() from inside of func_a().

def func_a():
print('Task triggered for function A')

def func_b():
print('Task triggered for function B')

func_b()
func_a()

To overcome this, we can return func_b as shown below:
def func_a():
print('Task triggered for function A')

def func_b():
print('Task triggered for function B')

# Notice, I am only providing the function name
return func_b


task_b = func_a()
task_b()