Call by value, Call By Reference

Everything in python is passed by Reference

Pass by value
Copy of the actual object is passed. Changing the value of the copy of the object will not change the value of the original object.

Pass by reference
Reference to the actual object is passed. Changing the value of the new object will change the value of the original object.

def appendNumber(arr):
    arr.append(4)
    
arr = [1, 2, 3]
appendNumber(arr)   #Call by reference
print(arr)  #Output: => [1, 2, 3, 4]
        

Function passing

Passing as Description
Function used as Object

def fun():
    return "Hello"
test = fun
print(test())        #Hello
                    
Function passed as argument to other function

def f(a,b):
    return a+b
def f1(var):
    print("hello",var)
f1(f(1,2))              #Passing function f() as argument of f1() //hello 3
                
Function as Mutable Objects

// MUTABLE OBJECTS(list,dictionary etc): Are shared between subsequent calls
def fun2(a, L=[]):
    L.append(a)
    return L
print(fun2(1))     #[1]
print(fun2(2))     #[1,2]
print(fun2(3))     #[1,2,3]
                

Variable no of arguments


def fun(*arg):
    for var in arg:
        print(var)
        return
fun ( 10, 20, 30 )
fun ( 1, 2 )
fun ( 'te', 90 )
        

Types of Functions

Type Description
Inner, function inside function

# Note that the order in which the inner functions are defined does not matter.
def parent():
    print("Parent")
    def fun1():
        print("fun1")
    def fun2():
        print("fun2")
    fun2()
    fun1()
parent()        #O/P: parent fun2 fun1
                

Function Arguments

Type Description
Arguments taking default value

def fun1(a, c='Again'):            #c is default arguments
    if a == 1:
        print('Test1')
    elif a==11 and c=='Hello':
        print('Test3')
fun1(1)                 #Test1
                
kwarg(Keyword Arguments) You can provide different sets of named parameters without having to define them explicitly in the function signature.

def fun(**kwargs):
    for key,value in kwargs.items():
        print(f"{key}: {value}")

fun(a='g', b=1.9, f="test")
                    
Decorator a decorator is a powerful design pattern that allows you to modify or extend the behavior of functions or methods without changing their original code
Decorators wrap around a function to enhance or modify its behavior Python builtin decorators: @staticmethod, @classmethod, and @property

def sonu(fun_arg):
    def wrapper():
        print("Before calling the function.")
        fun_arg()
        print("After calling the function.")
    return wrapper

# Decorator=sonu is called before fun & 
# fun is passed as argument to sonu
@sonu       #This is Decocator
def fun():
    print("Hello, World!")

fun()

# Output
Before calling the function.
Hello, World!
After calling the function.
                    
@staticmethod A static method is a method that belongs to a class rather than an instance of the class. It doesn't receive any implicit first argument (no self or cls)

class Calculator:
    @staticmethod
    def add_numbers(x, y):
        return x + y

# Can be called without creating an instance
result = Calculator.add_numbers(5, 10)
print(result)  # Outputs: 15                                
                                
@classmethod (used for alternative constructors) A class method receives the class itself as the first argument (cls), allowing it to modify class-level attributes.

class Employee:
    company = "Default Company"

    @classmethod
    def change_company(cls, new_company):       #cls is 1st arg(Means class)
        cls.company = new_company

    @classmethod
    def create_employee(cls, name):
        return cls(name)

    def __init__(self, name):
        self.name = name

# Can modify class-level attributes
Employee.change_company("Tech Corp")
                                
                                
@property Allows you to define methods that can be accessed like attributes, providing getter, setter, and deleter functionality.

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):               #self(instance)
        return self._radius

    @radius.setter
    def radius(self, value):
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius cannot be negative")

    @property
    def area(self):
        return 3.14 * self._radius ** 2

# Usage
circle = Circle(5)
print(circle.radius)  # Getter
circle.radius = 10    # Setter
print(circle.area)    # Computed property
                                
enumerate() function This is used to iterate over an iterable (Eg: list, tuple, or string). It returns a tuple(index, value of each item)

# enumerate(iterable, start=0(optional))

# Iterate over List=vector
my_list = ['apple', 'banana', 'cherry']
for index, fruit in enumerate(my_list):
    print(f"Index: {index}, Value: {fruit}")

# Iterate over hashmap=Dictionary
my_dict = {'a': 1, 'b': 2, 'c': 3}
for index, key in enumerate(my_dict):
    print(f"Index: {index}, Key: {key}, Value: {my_dict[key]}")
                    
Lambda / Anonymous function Can accept any number of arguments, but can only have a single expression. Use-case: Require an anonymous function for a short time period.

# Assigning lambda functions to a variable
mul = lambda a, b : a * b
print(mul(2, 5))    # output => 10

# Wrapping lambda functions inside another function
def myWrapper(n):
 return lambda a : a * n
mulFive = myWrapper(5)
print(mulFive(2))    # output => 10
                    
Generator Generator returns an iterator object which can be iterated over.

def simple_generator(n):
    for i in range(n):
        yield i

# Using the generator
gen = simple_generator(5)
for num in gen:
    print(num)
# Output
0
1
2
3
4
                    
How yeild works here? The yield keyword is crucial in creating generators. When yield is encountered:
  The function's execution is paused
  A value is returned to the caller
  The function's state is preserved
  Execution can resume from where it left off