Python Flashcards
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: B
What 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 MyClass
What is the difference between __new__ and __init__?
Output: Creating instance
- __new__ is responsible for creating a new instance of a class. It’s called before __init__ and is typically used when subclassing immutable types.
- __init__ initializes the instance created by __new__.
class MyClass: def \_\_new\_\_(cls): print("Creating instance") return super().\_\_new\_\_(cls) def \_\_init\_\_(self): print("Initializing instance") obj = MyClass() # Output: Creating instance # Initializing instance
How 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 function
Explain 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 ==?
- is checks for identity (whether two references point to the same object).
- == checks for equality (whether the values of two objects are the same).
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 + b