Q1). What is a Python decorator?

A Python decorator is a function that modifies the behavior of another function. It's used to add functionality to existing code. For example, if you have a function that prints a message, a decorator can be used to log when the function was called. Example:

def logger(func):
    def wrapper(*args, **kwargs):
        print('Function called')
        return func(*args, **kwargs)
    return wrapper

@logger
def hello():
    print('Hello, world!')

hello()
Output:
Function called
Hello, world!

Q2). What are Python generators?

Generators are functions that return an iterable set of items, one at a time, in a special way. They use `yield` instead of `return`. Generators are useful for processing large datasets or streams of data where you don’t want to store everything in memory. Example:

def count_up_to(max):
    count = 1
    while count <= max:
        yield count
        count += 1

for number in count_up_to(3):
    print(number)
Output:
1
2
3

Q3). What is the difference between `deepcopy` and `copy`?

`copy` creates a shallow copy of an object, meaning it copies the object but not the objects inside it. `deepcopy` creates a deep copy, meaning it copies the object and all objects inside it recursively. This is useful when you want to modify the copy without affecting the original. Example:

import copy

original_list = [1, [2, 3]]
shallow_copy = copy.copy(original_list)
deep_copy = copy.deepcopy(original_list)

shallow_copy[1].append(4)
print(original_list)
Output:
[1, [2, 3, 4]]
print(deep_copy)
Output:
[1, [2, 3]]

Q4). How does Python handle memory management?

Python uses automatic memory management, which includes garbage collection and reference counting. When objects are no longer in use, Python automatically frees up memory. Example:

import gc

def create_object():
    my_list = [1, 2, 3]

create_object()

gc.collect()
Output:
Memory freed by garbage collector

Q5). What are `__str__` and `__repr__` methods in Python?

`__str__` is used to define a user-friendly string representation of an object, while `__repr__` is used for debugging and development purposes, providing a detailed string representation of an object. Example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f'Person(name={self.name}, age={self.age})'

    def __repr__(self):
        return f'Person(name={self.name!r}, age={self.age!r})'

person = Person('John', 30)
print(person)
Output:
Person(name=John, age=30)
print(repr(person))
Output:
Person(name='John', age=30)

Q6). What is the Global Interpreter Lock (GIL) in Python?

The GIL is a mechanism that prevents multiple native threads from executing Python bytecodes at once. This can be a limitation for CPU-bound multi-threaded programs in Python, as it essentially makes sure only one thread executes Python code at a time. Example:

import threading

def count():
    for i in range(1000000):
        pass

thread1 = threading.Thread(target=count)
thread2 = threading.Thread(target=count)
thread1.start()
thread2.start()

thread1.join()
thread2.join()
The threads execute but only one thread runs Python code at a time due to the GIL.

Q7). What are metaclasses in Python?

Metaclasses are classes of classes. They define how classes behave. In Python, `type` is the default metaclass, but you can create your own metaclasses to customize class creation and behavior. Example:

class Meta(type):
    def __new__(cls, name, bases, dct):
        print('Creating a new class')
        return super(Meta, cls).
        __new__(cls, name, bases, dct)

class MyClass(metaclass=Meta):
    pass

Output:
Creating a new class

Q8). How do you optimize Python code for performance?

Optimizing Python code involves using efficient algorithms and data structures, minimizing the use of global variables, leveraging built-in functions and libraries, and profiling the code to identify bottlenecks. Example:

def optimize_example(data):
    return [x * 2 for x in data]

List comprehension is more efficient than using a loop for the same task

Q9). What is the purpose of Python decorators?

Decorators are used to modify the behavior of functions or methods without changing their actual code. They are often used for logging, access control, and performance measurement. Example:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print('Something is happening before the function is called.')
        result = func(*args, **kwargs)
        print('Something is happening after the function is called.')
        return result
    return wrapper

@my_decorator
def say_hello():
    print('Hello!')

say_hello()
Output:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.

Q10). How does Python handle memory management?

Python handles memory management through automatic garbage collection and reference counting. The garbage collector identifies and frees up memory that is no longer in use. Example:

import gc

gc.collect()
Forces garbage collection

Q11). What is the difference between `deepcopy` and `copy` in Python?

`copy` creates a shallow copy of an object, meaning only the top-level elements are copied. `deepcopy` creates a deep copy, where all nested objects are copied as well. Example:

import copy

original = {'a': [1, 2, 3]}
shallow_copy = copy.copy(original)
deep_copy = copy.deepcopy(original)

shallow_copy['a'].append(4)
Changes affect both shallow_copy and original

deep_copy['a'].append(5)
Only deep_copy is affected

Q12). How do you handle exceptions in Python?

Exceptions in Python are handled using `try`, `except`, `else`, and `finally` blocks. You write code inside the `try` block, catch exceptions in the `except` block, run code if no exception occurs in the `else` block, and execute code regardless of whether an exception occurred in the `finally` block. Example:

try:
    result = 10 / 0
except ZeroDivisionError:
    print('Cannot divide by zero.')
else:
    print('Division successful.')
finally:
    print('Execution complete.')

Output:
Cannot divide by zero.
Execution complete.

Q13). What are Python generators, and how do they work?

Generators are special functions that return an iterator. They use `yield` instead of `return` to produce a series of values lazily. This means they produce items one at a time and are more memory-efficient for large data sets. Example:

def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for number in count_up_to(5):
    print(number)

Output:
1
2
3
4
5

Q14). What is the difference between `is` and `==` in Python?

In Python, `is` checks for object identity, meaning it returns `True` if both variables point to the same object in memory. `==` checks for value equality, meaning it returns `True` if the values of both variables are equal, even if they are different objects in memory. Example:

a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  True, because values are equal
print(a is b)  False, because they are different objects

Q15). What are Python’s built-in data types?

Python has several built-in data types, including: - `int` (integer) - `float` (floating-point number) - `str` (string) - `list` (list) - `tuple` (tuple) - `set` (set) - `dict` (dictionary) Example:

number = 10  int
pi = 3.14  float
text = 'hello'  str
items = [1, 2, 3]  list
coordinates = (1, 2)  tuple
unique_numbers = {1, 2, 3}  set
person = {'name': 'Alice', 'age': 25}  dict

Q16). How do you use list comprehensions in Python?

List comprehensions provide a concise way to create lists. They consist of an expression followed by a `for` loop inside square brackets. They can also include `if` conditions. Example:

squares = [x ** 2 for x in range(5)]
Output: [0, 1, 4, 9, 16]

Q17). What is a lambda function in Python?

A lambda function is an anonymous function defined using the `lambda` keyword. It can have any number of arguments but only one expression. The result of the expression is returned. Example:

add = lambda x, y: x + y
print(add(5, 3))  Output: 8

Q18). What is the difference between `append` and `extend` methods in a list?

`append` adds its argument as a single element to the end of a list, while `extend` iterates over its argument adding each element to the list. Example:

lst = [1, 2, 3]
lst.append([4, 5])
print(lst)  Output: [1, 2, 3, [4, 5]]

lst = [1, 2, 3]
lst.extend([4, 5])
print(lst)  Output: [1, 2, 3, 4, 5]

Q19). How do you handle exceptions in Python?

Exceptions in Python are handled using `try`, `except`, `else`, and `finally` blocks. `try` contains the code that might raise an exception, `except` handles the exception, `else` runs if no exception occurs, and `finally` runs code that should execute no matter what. Example:

try:
    print(1 / 0)
except ZeroDivisionError:
    print('Cannot divide by zero')
finally:
    print('Execution complete')

Output:
Cannot divide by zero
Execution complete

Q20). What is a decorator in Python?

A decorator is a function that takes another function and extends its behavior without explicitly modifying it. They are used to wrap functions with additional functionality. Example:

def my_decorator(func):
    def wrapper():
        print('Something is happening before the function is called.')
        func()
        print('Something is happening after the function is called.')
    return wrapper

@my_decorator
def say_hello():
    print('Hello!')

say_hello()

Output:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.

Q21). What is the purpose of the `self` keyword in Python classes?

The `self` keyword represents the instance of the class and is used to access variables and methods associated with the instance. It differentiates between instance attributes and method arguments. Example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

person = Person('Alice', 30)
print(person.name)  Output: Alice

Q22). How does Python handle memory management?

Python handles memory management automatically using a built-in garbage collector. It keeps track of objects and their references, and when an object is no longer needed, it is automatically deallocated. Example:

import gc

def create_large_object():
    large_list = [i for i in range(1000000)]

create_large_object()

gc.collect()  Force garbage collection

Q23). What are Python’s data encapsulation mechanisms?

Python uses access control mechanisms like public, protected, and private to encapsulate data: - **Public**: Accessible from anywhere. - **Protected**: Indicated by a single underscore `_`, meant for internal use. - **Private**: Indicated by double underscores `__`, not directly accessible from outside the class. Example:

class MyClass:
    def __init__(self):
        self.public_var = 'I am public'
        self._protected_var = 'I am protected'
        self.__private_var = 'I am private'

obj = MyClass()
print(obj.public_var)   Output: I am public
print(obj._protected_var) Output: I am protected
print(obj.__private_var)  AttributeError

Q24). What is a lambda function in Python?

A lambda function is an anonymous function defined using the `lambda` keyword. It can have any number of arguments but only one expression. It returns the result of the expression. Example:

add = lambda x, y: x + y

print(add(5, 3))  Output: 8

Q25). What are Python’s built-in data types?

Python has several built-in data types, including: - **Numbers**: `int`, `float`, `complex` - **Sequences**: `list`, `tuple`, `range` - **Mappings**: `dict` - **Sets**: `set`, `frozenset` - **Boolean**: `bool` - **None**: `NoneType` Example:

int_var = 10
float_var = 10.5
list_var = [1, 2, 3]
dict_var = {'a': 1, 'b': 2} 

print(int_var, float_var, list_var, dict_var)

Output:
10 10.5 [1, 2, 3] {'a': 1, 'b': 2}

Q26). How can you merge two dictionaries in Python?

You can merge two dictionaries using the `update()` method or using the `**` unpacking operator (Python 3.5+). Example:

dict1 = {'a': 1}
dict2 = {'b': 2}

dict1.update(dict2)
print(dict1)  Output: {'a': 1, 'b': 2}

dict3 = {**dict1, **dict2}
print(dict3)  Output: {'a': 1, 'b': 2}

Q27). How do you perform string formatting in Python?

String formatting in Python can be done using the `%` operator, `str.format()` method, or f-strings (Python 3.6+). Example:

Using % operator
name = 'Alice'
print('Hello, %s!' % name)  Output: Hello, Alice!

Using str.format()
print('Hello, {}!'.format(name))  Output: Hello, Alice!

Using f-strings
print(f'Hello, {name}!')  Output: Hello, Alice!

Q28). What is the difference between `deepcopy` and `copy` in Python?

The `copy` module provides `copy()` for shallow copying and `deepcopy()` for deep copying. A shallow copy creates a new object but does not create copies of nested objects. A deep copy creates a new object and recursively copies all objects found in the original object. Example:

import copy

original = {'a': 1, 'b': {'c': 2} 

shallow_copy = copy.copy(original)
deep_copy = copy.deepcopy(original)

print(shallow_copy, deep_copy)

Output:
{'a': 1, 'b': {'c': 2}} {'a': 1, 'b': {'c': 2}}

Q29). How do you handle exceptions in Python?

Exceptions in Python are handled using `try`, `except`, `else`, and `finally` blocks. The `try` block contains code that might raise an exception. The `except` block contains code to handle the exception. The `else` block executes if no exception occurs, and the `finally` block executes code regardless of whether an exception occurred. Example:

try:
    print('Trying...')
    result = 10 / 0
except ZeroDivisionError:
    print('Cannot divide by zero')
else:
    print('Division successful')
finally:
    print('End of try-except')

Output:
Trying...
Cannot divide by zero
End of try-except