Explain the Global Interpreter Lock (GIL) in Python. How does it affect multithreading?
The GIL is a mutex that protects access to Python objects, preventing multiple native threads from executing Python bytecodes at once. This lock is necessary because Python’s memory management isn’t thread-safe. The GIL allows only one thread to execute Python code at a time, even if on multi-core systems.
This affects CPU-bound operations because even in multi-threaded programs, only one thread can run Python code at a time. I/O-bound tasks, like file operations or network communication, benefit from multithreading as these tasks release the GIL during I/O operations.
import threading
def cpu_bound_task():
x = 0
for i in range(10**7):
x += i
Running two threads
t1 = threading.Thread(target=cpu_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()In this case, despite using threads, only one thread executes at a time because of the GIL.
What are Python’s memory management features?
Python uses reference counting and a garbage collector to manage memory. When an object’s reference count drops to zero, it is automatically deallocated. Python’s garbage collector also handles cyclic references using a generational garbage collection mechanism.
import sys a = [] b = a print(sys.getrefcount(a)) # Output: 3, since there is one reference for 'a', 'b', and internal reference in `getrefcount()`
When the reference count of a drops to zero, it is collected by the garbage collector.
Explain Python’s Method Resolution Order (MRO).
MRO determines the order in which base classes are searched when calling a method. Python uses the C3 linearization algorithm to determine this order. The MRO can be checked using the __mro__ attribute or mro() method of a class.
class A:
def process(self):
print("A")
class B(A):
def process(self):
print("B")
class C(A):
def process(self):
print("C")
class D(B, C):
pass
print(D.mro()) # [D, B, C, A, object]
d = D()
d.process() # Output: BWhat is the difference between deepcopy and copy?
copy.copy() creates a shallow copy of an object, meaning it only copies the object itself, not nested objects.
copy.deepcopy() creates a deep copy, meaning it copies the object and all objects it contains.
import copy a = [1, [2, 3], 4] b = copy.copy(a) c = copy.deepcopy(a) a[1][0] = 99 print(b) # Shallow copy: [1, [99, 3], 4] print(c) # Deep copy: [1, [2, 3], 4]
Describe how Python’s asyncio library works.
asyncio provides a framework for writing asynchronous (non-blocking) code using async and await. It’s useful for I/O-bound tasks like file reading, network requests, and database queries.
import asyncio
async def fetch_data():
print('Start fetching...')
await asyncio.sleep(2)
print('Done fetching')
return 'data'
async def main():
result = await fetch_data()
print(result)
Run the event loop
asyncio.run(main())Here, the await keyword pauses the execution of the fetch_data() coroutine and allows other tasks to run while waiting.
What are metaclasses in Python?
Metaclasses are classes of classes; they define how classes behave. A class in Python is an instance of a metaclass. Normally, type is the metaclass used to create classes.
class Meta(type):
def \_\_new\_\_(cls, name, bases, dct):
print(f'Creating class {name}')
return super().\_\_new\_\_(cls, name, bases, dct)
class MyClass(metaclass=Meta):
pass
Output: Creating class MyClassWhat is the difference between __new__ and __init__?
Output: Creating instance
class MyClass:
def \_\_new\_\_(cls):
print("Creating instance")
return super().\_\_new\_\_(cls)
def \_\_init\_\_(self):
print("Initializing instance")
obj = MyClass()
# Output: Creating instance
# Initializing instanceHow are Python decorators implemented?
Decorators are functions that modify the behavior of another function. They take a function as input and return a new function.
def my_decorator(func):
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
# Output: Something before the function
# Hello!
# Something after the functionExplain how Python handles exceptions.
Python uses try-except blocks for exception handling. You can catch multiple exceptions, use the finally block for cleanup, and raise custom exceptions.
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Caught an exception: {e}")
else:
# Code to run if there was no exception
finally:
print("This block is always executed")What is the purpose of the @dataclass decorator in Python?
The @dataclass decorator automatically generates special methods like __init__(), __repr__(), and __eq__() for a class. It reduces boilerplate for simple data structures.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(1, 2)
print(p1) # Output: Point(x=1, y=2)Explain how list comprehensions and generator expressions work.
List comprehensions are a concise way to create lists. Generator expressions work similarly but return an iterator instead of a list, which is more memory-efficient.
# List comprehension squares = [x**2 for x in range(10)] Generator expression squares_gen = (x**2 for x in range(10)) print(next(squares_gen)) # Output: 0
What is the difference between is and ==?
a = [1, 2, 3] b = a c = [1, 2, 3] print(a is b) # True print(a == c) # True
What are Python’s magic methods?
Magic methods are special methods that start and end with double underscores. They allow customization of Python’s built-in operations for objects.
class MyNumber:
def \_\_init\_\_(self, value):
self.value = value
def \_\_add\_\_(self, other):
return MyNumber(self.value + other.value)
def \_\_repr\_\_(self):
return f"MyNumber({self.value})"
num1 = MyNumber(10)
num2 = MyNumber(20)
print(num1 + num2) # Output: MyNumber(30)Explain the difference between __getattr__ and __getattribute__.
__getattr__ is called when an attribute is not found in an object.
__getattribute__ is called every time an attribute is accessed (even if it exists).
class MyClass:
def \_\_getattr\_\_(self, name):
return f"{name} not found"
def \_\_getattribute\_\_(self, name):
print(f"Accessing {name}")
return object.\_\_getattribute\_\_(self, name)
obj = MyClass()
print(obj.existing_attr) # Calls \_\_getattribute\_\_
print(obj.non_existent_attr) # Calls \_\_getattr\_\_How does Python handle dynamic typing?
Python is a dynamically typed language, meaning that the type of a variable is determined at runtime rather than at compile time. This approach provides significant flexibility and ease of use but also requires developers to be mindful of potential type-related errors that can emerge during execution.
x = 5 # x is an integer x = "hello" # x is now a string
To enforce stricter typing, Python 3.5+ supports type hints:
def add(a: int, b: int) -> int:
return a + bWhat are slots in Python?
__slots__ limit the attributes a class can have, improving memory efficiency by preventing the creation of __dict__ for each instance.
class MyClass:
\_\_slots\_\_ = ['x', 'y']
obj = MyClass()
obj.x = 10
obj.y = 20Explain the difference between mutable and immutable objects.
b[0] = 10 # This would raise an error, as tuples are immutable.
a = [1, 2, 3] a[0] = 10 # Mutable b = (1, 2, 3) # b[0] = 10 # This would raise an error, as tuples are immutable.
How does Python’s itertools library work?
itertools provides efficient looping tools. Common functions include chain(), combinations(), and groupby().
from itertools import chain, combinations
Chain: Flatten multiple iterables
for i in chain([1, 2], [3, 4]):
print(i) # Output: 1 2 3 4
Combinations
print(list(combinations([1, 2, 3], 2))) # Output: [(1, 2), (1, 3), (2, 3)]What is monkey patching?
Monkey patching is the dynamic modification of a class or module at runtime. It can be useful but is generally considered bad practice because it can make code harder to understand and maintain.
class MyClass:
def greet(self):
return "Hello"
Monkey patching
def new_greet():
return "Hi"
MyClass.greet = new_greet
obj = MyClass()
print(obj.greet()) # Output: HiDiscuss the heapq library.
heapq implements a binary heap, which is useful for priority queues. The smallest element is always at the root.
import heapq nums = [1, 8, 3, 5, 4] heapq.heapify(nums) print(heapq.heappop(nums)) # Output: 1 (smallest element)
Explain Python’s multiprocessing library.
multiprocessing allows parallel execution of code by using separate processes. It avoids the GIL limitation present in threading by creating new processes.
import multiprocessing
def worker():
print("Worker process")
if \_\_name\_\_ == "\_\_main\_\_":
p = multiprocessing.Process(target=worker)
p.start()
p.join()What are context managers in Python?
Context managers manage resources (like file I/O). The with statement ensures proper acquisition and release of resources.
with open('file.txt', 'w') as f:
f.write('Hello, World!')
# File is automatically closed after exiting the with block.What is the difference between yield and return?
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator()
print(next(gen)) # Output: 1
print(next(gen)) # Output: 2Explain the collections module.
The collections module provides specialized data structures such as defaultdict, namedtuple, and deque.
from collections import defaultdict, namedtuple, deque
defaultdict
dd = defaultdict(int)
dd['a'] += 1
print(dd['a']) # Output: 1
namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y) # Output: 1 2
deque
d = deque([1, 2, 3])
d.appendleft(0)
print(d) # Output: deque([0, 1, 2, 3])