OOP Fundamentals Quiz
Quiz
__init__ method in a Python class?__init__ is the initializer that runs automatically when an object is created. Its job is to set up the initial state by assigning instance attributes via self. Memory allocation is handled by __new__, which runs before __init__.__init__ is the initializer that runs automatically when an object is created. Its job is to set up the initial state by assigning instance attributes via self. Memory allocation is handled by __new__, which runs before __init__.Person("Alice", 30) is called.__init__ is shared across all instances, while an instance attribute set via self.attr = value inside __init__ is unique to each object.species = 'Homo sapiens' vs. self.name = name each live.@classmethod receives _____ as its first parameter instead of self, giving it access to the class rather than an instance.cls as the first parameter to refer to the class itself. This allows them to access class attributes and create instances via cls(...), making them ideal for alternative constructors.cls as the first parameter to refer to the class itself. This allows them to access class attributes and create instances via cls(...), making them ideal for alternative constructors.class Dog:
tricks = []
def add_trick(self, trick):
self.tricks.append(trick)
d1 = Dog()
d2 = Dog()
d1.add_trick("roll over")
print(len(d2.tricks))tricks is a class attribute — a single list shared by all instances. When d1.add_trick() calls self.tricks.append(), it mutates the shared class-level list (it does not reassign self.tricks). So d2.tricks reflects the same mutation, and its length is 1. The fix is to move self.tricks = [] inside __init__.tricks is a class attribute — a single list shared by all instances. When d1.add_trick() calls self.tricks.append(), it mutates the shared class-level list (it does not reassign self.tricks). So d2.tricks reflects the same mutation, and its length is 1. The fix is to move self.tricks = [] inside __init__.self.tricks.append() create a new list or mutate an existing one?@classmethod?@classmethod receives the class as cls, making it ideal for alternative constructors like Person.from_birth_year('Alice', 1990). Utility functions that need neither instance nor class state are better served by @staticmethod. Method overriding doesn’t require any decorator.@classmethod receives the class as cls, making it ideal for alternative constructors like Person.from_birth_year('Alice', 1990). Utility functions that need neither instance nor class state are better served by @staticmethod. Method overriding doesn’t require any decorator.from_birth_year is used in the notes.@staticmethod are true? (Select all that apply)ClassName.method() or instance.method(). They are perfect for utility/helper functions that belong conceptually to a class but don’t need its state.ClassName.method() or instance.method(). They are perfect for utility/helper functions that belong conceptually to a class but don’t need its state.@staticmethod removes compared to a regular or class method.super() always refers exclusively to the direct parent class.super() follows the Method Resolution Order (MRO), not just the immediate parent. In multiple inheritance, super() calls the next class in the MRO chain (computed via C3 linearization), which may not be the direct parent. This cooperative design is essential for diamond inheritance to work correctly.super() follows the Method Resolution Order (MRO), not just the immediate parent. In multiple inheritance, super() calls the next class in the MRO chain (computed via C3 linearization), which may not be the direct parent. This cooperative design is essential for diamond inheritance to work correctly.class D(B, C) where both B and C inherit from A.class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C):
pass
d = D()
print(d.method())D is [D, B, C, A, object] (C3 linearization). When d.method() is called, Python searches left to right: D has no method, B has one — it returns "B". C and A are never reached. You can verify with print(D.mro()).D is [D, B, C, A, object] (C3 linearization). When d.method() is called, Python searches left to right: D has no method, B has one — it returns "B". C and A are never reached. You can verify with print(D.mro()).D(B, C)?@dataclass generate by default (without extra arguments)? (Select all that apply)@dataclass generates __init__, __repr__, and __eq__ by default. __str__ is NOT generated — Python falls back to __repr__ when printing. Comparison ordering methods like __lt__ require @dataclass(order=True).@dataclass generates __init__, __repr__, and __eq__ by default. __str__ is NOT generated — Python falls back to __repr__ when printing. Comparison ordering methods like __lt__ require @dataclass(order=True).class Temperature:
@property
def celsius(self):
return self._celsius
@_____.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value@celsius.setter. This links the setter to the celsius property defined above it. The method name must also match. Using a different name creates an unlinked setter that Python won’t connect to the property.@celsius.setter. This links the setter to the celsius property defined above it. The method name must also match. Using a different name creates an unlinked setter that Python won’t connect to the property.@property that has no setter defined?AttributeError: can't set attribute. This is the intended behavior — it enforces read-only access. To make it writable, you must explicitly define a @property_name.setter.AttributeError: can't set attribute. This is the intended behavior — it enforces read-only access. To make it writable, you must explicitly define a @property_name.setter.self.__secret in class MyClass is truly private and cannot be accessed from outside the class.self.__secret in MyClass is stored as _MyClass__secret. This prevents accidental collisions in subclasses but does NOT enforce true privacy. You can still access it from outside as obj._MyClass__secret. Python’s philosophy is ‘consenting adults’: naming conventions communicate intent, but there are no hard access restrictions.self.__secret in MyClass is stored as _MyClass__secret. This prevents accidental collisions in subclasses but does NOT enforce true privacy. You can still access it from outside as obj._MyClass__secret. Python’s philosophy is ‘consenting adults’: naming conventions communicate intent, but there are no hard access restrictions.private. What mechanism does it use instead?FrozenInstanceError, pass _____=True to the @dataclass decorator.@dataclass(frozen=True) prevents any attribute assignment after the object is created, making the instance behave like a named tuple. Frozen dataclasses are also hashable by default, allowing them to be used as dictionary keys or set members.@dataclass(frozen=True) prevents any attribute assignment after the object is created, making the instance behave like a named tuple. Frozen dataclasses are also hashable by default, allowing them to be used as dictionary keys or set members.sorted()?sorted() uses __lt__ (less than) as the default comparison operator. While __eq__ is useful for equality checks, it’s not required for sorting. __hash__ is for set/dict membership. You can also use functools.total_ordering to fill in all comparison methods from just __eq__ and one other.sorted() uses __lt__ (less than) as the default comparison operator. While __eq__ is useful for equality checks, it’s not required for sorting. __hash__ is for set/dict membership. You can also use functools.total_ordering to fill in all comparison methods from just __eq__ and one other.__str__ and __repr__?__str__ is the ‘pretty’ representation for end users — called by print() and str(). __repr__ is for developers and debugging — called in the REPL, by repr(), and as a fallback when __str__ is absent. A good __repr__ ideally returns a string that could recreate the object: Person('Alice', 30).__str__ is the ‘pretty’ representation for end users — called by print() and str(). __repr__ is for developers and debugging — called in the REPL, by repr(), and as a fallback when __str__ is absent. A good __repr__ ideally returns a string that could recreate the object: Person('Alice', 30).class Counter:
count = 0
def __init__(self):
Counter.count += 1
@classmethod
def get_count(cls):
return cls.count
a = Counter()
b = Counter()
c = Counter()
print(Counter.get_count())Counter() call triggers __init__, which increments the class attribute Counter.count. After three instantiations, count is 3. get_count() returns cls.count, which is 3. Note: using Counter.count += 1 (not self.count += 1) correctly modifies the class attribute rather than creating a shadowing instance attribute.Counter() call triggers __init__, which increments the class attribute Counter.count. After three instantiations, count is 3. get_count() returns cls.count, which is 3. Note: using Counter.count += 1 (not self.count += 1) correctly modifies the class attribute rather than creating a shadowing instance attribute.Counter() called, and where does count live?class D(B, C) where class B(A) and class C(A), arrange the Method Resolution Order (MRO) for class D from first searched to last searched:[D, B, C, A, object]. Python searches left to right, checking each class for the requested attribute. D itself is checked first, then its leftmost parent B, then C, then their shared ancestor A, and finally the base object. This prevents A’s methods from being found before C’s in a diamond pattern.[D, B, C, A, object]. Python searches left to right, checking each class for the requested attribute. D itself is checked first, then its leftmost parent B, then C, then their shared ancestor A, and finally the base object. This prevents A’s methods from being found before C’s in a diamond pattern.The ability for different objects to respond to the same interface (method call) in their own type-specific way.
Python uses duck typing: if an object has the right method, it works — regardless of its type. Example: shape.area() works on both Rectangle and Circle because each defines its own area() implementation. You don’t need explicit interfaces or type checks.
Did you get it right?
Name mangling transforms __attr (double underscore prefix) into _ClassName__attr at compile time.
Problem it solves: prevents accidental attribute name collisions in subclasses. If Parent uses self.__data and Child also defines self.__data, they don’t clash — they become _Parent__data and _Child__data respectively.
Important: this is NOT true privacy. The attribute is still accessible as obj._ClassName__attr from outside.
Did you get it right?
__post_init__ is called after all fields have been initialized by the generated __init__, allowing you to perform validation or compute derived attributes.__init__ assigns all declared fields first, then automatically calls __post_init__ if it’s defined. This is the correct place for cross-field validation or initializing fields that depend on other fields (like setting count = len(self.items) in an Inventory dataclass).__init__ assigns all declared fields first, then automatically calls __post_init__ if it’s defined. This is the correct place for cross-field validation or initializing fields that depend on other fields (like setting count = len(self.items) in an Inventory dataclass).__init__.class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary)
self.department = department
mgr = Manager("Alice", 100000, "Engineering")
print(hasattr(mgr, "salary"))super().__init__(name, salary) calls Employee.__init__, which sets self.salary = salary on the mgr instance. So mgr.salary exists. hasattr(mgr, 'salary') returns True. This demonstrates why calling super().__init__() is essential — skipping it means the parent’s attributes are never set.super().__init__(name, salary) calls Employee.__init__, which sets self.salary = salary on the mgr instance. So mgr.salary exists. hasattr(mgr, 'salary') returns True. This demonstrates why calling super().__init__() is essential — skipping it means the parent’s attributes are never set.super().__init__() actually run Employee.__init__ on the mgr instance?field(default_factory=list) instead of items: list = [] for list fields?items: list = [] would make all instances share the same list object — a classic Python gotcha. field(default_factory=list) calls list() fresh for each new instance, giving every object its own independent list. This same issue applies to any mutable default (dicts, sets, custom objects).items: list = [] would make all instances share the same list object — a classic Python gotcha. field(default_factory=list) calls list() fresh for each new instance, giving every object its own independent list. This same issue applies to any mutable default (dicts, sets, custom objects).class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)v1 + v2 is syntactic sugar for v1.__add__(v2). Python translates arithmetic operators to dunder method calls. __add__ receives other = v2 and returns a new Vector(1+3, 2+4) = Vector(4, 6). Without __add__, the + operator would raise a TypeError.v1 + v2 is syntactic sugar for v1.__add__(v2). Python translates arithmetic operators to dunder method calls. __add__ receives other = v2 and returns a new Vector(1+3, 2+4) = Vector(4, 6). Without __add__, the + operator would raise a TypeError.a + b into a method call. Which method?Playlist class needs to support len(playlist), playlist[0], and "Song" in playlist. Which dunder methods must it implement? (Select all that apply)len() calls __len__. Index access obj[i] calls __getitem__. The in operator calls __contains__. __iter__ enables for item in obj iteration (not needed here). __setitem__ handles assignment obj[i] = value (also not needed here). Each built-in operation maps to a specific dunder method.len() calls __len__. Index access obj[i] calls __getitem__. The in operator calls __contains__. __iter__ enables for item in obj iteration (not needed here). __setitem__ handles assignment obj[i] = value (also not needed here). Each built-in operation maps to a specific dunder method.len(), [], and in.self._balance) conventionally signal in Python?obj._balance is perfectly accessible from outside. Name mangling (renaming) only applies to double underscores (__). This reflects Python’s philosophy: trust developers to respect conventions rather than enforce hard access restrictions.obj._balance is perfectly accessible from outside. Name mangling (renaming) only applies to double underscores (__). This reflects Python’s philosophy: trust developers to respect conventions rather than enforce hard access restrictions.class ManagedResource:
def __enter__(self):
print("Acquired")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Released")
return False
with ManagedResource() as r:
print("Using")with executes __enter__ first (prints “Acquired”), then runs the block body (prints “Using”), then always calls __exit__ on exit (prints “Released”) — whether the block succeeded or raised an exception. This guaranteed cleanup is the entire point of the context manager protocol.with executes __enter__ first (prints “Acquired”), then runs the block body (prints “Using”), then always calls __exit__ on exit (prints “Released”) — whether the block succeeded or raised an exception. This guaranteed cleanup is the entire point of the context manager protocol.__exit__ returns True, any exception that occurred inside the with block is suppressed and execution continues normally after the with statement.__exit__ controls exception propagation. True = suppress the exception (swallow it). False or None = re-raise it. This is why most context managers return False — they want to clean up resources but not silently hide errors. Returning True is intentional only when the class is designed to handle specific exceptions.__exit__ controls exception propagation. True = suppress the exception (swallow it). False or None = re-raise it. This is why most context managers return False — they want to clean up resources but not silently hide errors. Returning True is intentional only when the class is designed to handle specific exceptions.True?