Unit 4: More on OOP concepts - Subjective Questions
ECAP776 • Practice Questions with Detailed Answers
20 questions
Define function overloading. Does Python support traditional function overloading based on parameter types or parameter count?
Function overloading means defining multiple functions with the same name but different parameter lists so that the appropriate version is selected based on the arguments.
Python does not support traditional compile-time function overloading. If multiple functions with the same name are defined in the same scope, the latest definition replaces the earlier definitions.
For example:
def display(value):
return str(value)
def display(value, prefix):
return prefix + str(value)
Only the second definition remains available. Calling display(10) therefore raises a TypeError.
Python achieves similar behavior using:
- Default arguments
- Variable-length arguments such as
*argsand**kwargs - Type inspection
functools.singledispatch
Thus, Python provides flexible alternatives rather than traditional signature-based overloading.
Explain how default arguments can be used to simulate function overloading in Python, with an example.
Default arguments allow a function to accept different numbers of arguments by assigning initial values to optional parameters.
Example:
def area(length, breadth=None):
if breadth is None:
return length * length
return length * breadth
In this function:
area(5)treats the shape as a square and returns25.area(5, 3)treats the shape as a rectangle and returns15.
The single function therefore performs different operations depending on the number of supplied arguments. This resembles function overloading, although Python is actually executing one function definition with an optional parameter.
Using None as the default value is often safer than using a meaningful numeric value because it clearly indicates that the argument was omitted.
Describe how *args and **kwargs help implement flexible or overloaded-like functions in Python.
*args and **kwargs allow a function to receive a variable number of arguments.
*argscollects extra positional arguments into a tuple.**kwargscollects extra keyword arguments into a dictionary.
Example:
def calculate(*args, **kwargs):
operation = kwargs.get("operation", "sum")
if operation == "sum":
return sum(args)
if operation == "product":
result = 1
for value in args:
result *= value
return result
raise ValueError("Unsupported operation")
Usage:
calculate(2, 3, 4)
calculate(2, 3, 4, operation="product")
The function can process different numbers of values and select behavior through a keyword. This provides overloaded-like flexibility, but the programmer must validate the arguments and handle unsupported combinations explicitly.
Compare the use of default arguments, *args, and explicit type checking as alternatives to function overloading in Python.
Python offers several alternatives to traditional function overloading:
- Default arguments: Best when the operation has a fixed set of optional parameters. They produce clear function signatures but may become complicated when many combinations are permitted.
*argsand**kwargs: Best when the number of arguments is variable or not known in advance. They are highly flexible, but the accepted interface may be less obvious and requires manual validation.- Explicit type checking: Uses tools such as
isinstance()to select behavior according to argument types. It can be useful for a small number of cases but may produce long conditional structures and tightly coupled code.
For example, a function may use isinstance(value, str) to process text differently from numbers. However, excessive type checking conflicts with Python's preference for polymorphism and duck typing.
The preferred technique depends on whether behavior varies by omitted parameters, argument quantity, or argument type.
What is functools.singledispatch? Explain how it provides type-based function dispatch in Python.
functools.singledispatch converts a normal function into a generic function whose implementation is selected according to the type of its first argument.
Example:
from functools import singledispatch
@singledispatch
def describe(value):
return f"Object: {value}"
@describe.register
def _(value: int):
return f"Integer: {value}"
@describe.register
def _(value: list):
return f"List with {len(value)} items"
Key points:
- The undecorated-style base implementation acts as the fallback.
- Registered implementations handle specified types.
- Dispatch considers the first argument's runtime type and its inheritance hierarchy.
- It is runtime dispatch, not traditional compile-time overloading.
For methods, Python also provides functools.singledispatchmethod, which applies a similar idea while ignoring self or cls when choosing the implementation.
Explain operator overloading in Python. Why is it useful in user-defined classes?
Operator overloading is the process of defining how built-in operators behave when applied to objects of a user-defined class. Python implements this through special methods, also called dunder methods.
Examples include:
__add__()for+__sub__()for-__mul__()for*__eq__()for==__lt__()for<
If a + b is evaluated, Python generally attempts to call a.__add__(b).
Operator overloading is useful because it:
- Gives domain objects natural and readable syntax.
- Allows objects to interact with Python operators.
- Supports polymorphic behavior.
- Makes classes such as vectors, matrices, dates, and monetary values easier to use.
Operators should retain intuitive meanings. For example, + should normally combine or add objects rather than perform an unrelated task.
Design a Vector2D class that overloads the + operator. Explain what happens when two vector objects are added.
A two-dimensional vector may overload + by defining __add__():
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
return Vector2D(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
Example:
first = Vector2D(2, 3)
second = Vector2D(4, 5)
result = first + second
Python translates the addition approximately into first.__add__(second). The method adds corresponding components, so the result is Vector2D(6, 8).
Returning a new object avoids unexpectedly modifying either operand. Returning NotImplemented for an unsupported type also allows Python to try a reflected operation before eventually raising TypeError.
Distinguish among __add__(), __radd__(), and __iadd__() in Python operator overloading.
These methods represent three related forms of addition:
__add__(self, other)handles ordinary addition such asa + b, withaas the left operand.__radd__(self, other)handles reflected addition. Python may tryb.__radd__(a)whena.__add__(b)is unavailable or returnsNotImplemented, subject to subclass dispatch rules.__iadd__(self, other)handles augmented assignment such asa += b.
__iadd__() may mutate and return self, which is common for mutable objects. It may instead return a new object, which is appropriate for immutable objects. If __iadd__() is unavailable or returns NotImplemented, Python normally falls back to ordinary addition and assignment.
These methods permit classes to support left-hand, right-hand, and in-place forms of the same operator while correctly handling mixed operand types.
Explain the purpose of returning NotImplemented from an overloaded operator method. How does it differ from raising NotImplementedError?
NotImplemented is a special singleton value that an operator method should return when it does not support the other operand's type.
For an expression such as left + right:
- Python may first call
left.__add__(right). - If that method returns
NotImplemented, Python may try the corresponding reflected method,right.__radd__(left). - If neither operand supports the operation, Python raises
TypeError.
NotImplementedError, by contrast, is an exception generally raised by methods whose implementation is intentionally absent, often in an incomplete base-class interface.
Therefore:
- Use
return NotImplementedfor unsupported operand combinations in binary operator methods. - Use
raise NotImplementedErroronly to indicate that a method itself must be implemented or has not yet been implemented.
Raising NotImplementedError inside an operator method prevents Python's normal reflected-operation negotiation.
Describe how comparison operators can be overloaded in Python. Include the roles of __eq__() and __lt__().
Comparison operators are overloaded using special methods:
__eq__()implements==.__ne__()implements!=.__lt__()implements<.__le__()implements<=.__gt__()implements>.__ge__()implements>=.
Example:
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.name == other.name and self.score == other.score
def __lt__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.score < other.score
Here, equality compares both attributes, while < orders students by score. Defining comparison methods enables operations such as equality tests and sorting. The chosen comparison rules should be consistent and clearly documented.
What is the relationship between __eq__() and __hash__() when operator overloading is used for value equality?
__eq__() defines when two objects are considered equal, while __hash__() supplies the integer hash used by hash-based collections such as dictionaries and sets.
The essential rule is:
- If
a == b, thenhash(a)must equalhash(b).
When a class defines __eq__() but does not provide a compatible __hash__(), Python commonly makes instances unhashable by setting __hash__ to None. This prevents mutable or value-equal objects from corrupting set and dictionary behavior.
A suitable implementation for an immutable object is:
def __hash__(self):
return hash((self.x, self.y))
The same attributes used by __eq__() should normally be used to calculate the hash. Attributes involved in hashing must not change while the object is stored in a dictionary or set.
Explain how the @functools.total_ordering decorator reduces the work needed to overload ordering operators.
@functools.total_ordering is a class decorator that generates missing ordering methods from a smaller set of explicitly defined methods.
To use it, a class must define:
__eq__(), and- At least one ordering method, such as
__lt__(),__le__(),__gt__(), or__ge__().
Example:
from functools import total_ordering
@total_ordering
class Item:
def __init__(self, price):
self.price = price
def __eq__(self, other):
if not isinstance(other, Item):
return NotImplemented
return self.price == other.price
def __lt__(self, other):
if not isinstance(other, Item):
return NotImplemented
return self.price < other.price
Python derives the other ordering operations from these definitions. This reduces duplication, although explicitly implementing all methods can provide faster execution and clearer stack traces in performance-sensitive code.
Differentiate between operator overloading and method overriding in Python.
Operator overloading and method overriding are different forms of polymorphism.
Operator overloading:
- Defines operator behavior for class objects.
- Uses special methods such as
__add__(),__mul__(), and__eq__(). - Example: defining how two
Vectorobjects behave with+.
Method overriding:
- Occurs when a subclass supplies its own implementation of a method inherited from a base class.
- Uses the same method name and a compatible calling contract.
- Example: a
Dogclass replacing an inheritedspeak()implementation.
Operator overloading focuses on expressions involving operators, whereas method overriding focuses on inherited behavior. Both support polymorphism because the executed operation depends on the runtime type of the participating object.
Define method overriding and illustrate it using a base class and a derived class.
Method overriding occurs when a subclass defines a method with the same name as a method inherited from its base class, replacing or extending the inherited behavior for subclass instances.
Example:
class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Bark"
animal = Animal()
dog = Dog()
animal.speak() returns "Some sound", while dog.speak() returns "Bark".
When speak() is called on a Dog object, Python searches the Dog class before Animal, so it finds the overridden implementation first. This enables runtime polymorphism: different object types can respond differently to the same method call while sharing a common interface.
Explain the role of super() in an overridden method. Why is it preferable to directly naming the parent class in many situations?
super() returns a proxy that delegates method lookup to the next class in the method resolution order (MRO). It is commonly used when an overriding method must extend rather than completely replace inherited behavior.
Example:
class Employee:
def __init__(self, name):
self.name = name
class Manager(Employee):
def __init__(self, name, department):
super().__init__(name)
self.department = department
Benefits of super() include:
- Avoiding hard-coded references to a particular parent class.
- Supporting cooperative multiple inheritance.
- Following the class's MRO correctly.
- Making code easier to maintain if the inheritance hierarchy changes.
Calling Employee.__init__(self, name) may work in simple single inheritance, but it bypasses cooperative MRO behavior and can cause a class to be called twice or skipped in multiple-inheritance hierarchies.
Explain how Python's method resolution order (MRO) affects method overriding in multiple inheritance.
The method resolution order specifies the sequence in which Python searches classes for an attribute or method. In multiple inheritance, Python uses the C3 linearization algorithm to produce a consistent order that respects inheritance relationships and local parent ordering.
Example:
class A:
def show(self):
return "A"
class B(A):
def show(self):
return "B"
class C(A):
def show(self):
return "C"
class D(B, C):
pass
For D, the MRO is normally D, B, C, A, object. Therefore, D().show() finds B.show() first and returns "B".
The MRO can be inspected with D.mro() or D.__mro__. A zero-argument super() advances to the next class in this order, not necessarily to a class's immediate textual parent. This is essential for cooperative multiple inheritance.
Describe cooperative method overriding in multiple inheritance and state the conditions required for it to work correctly.
In cooperative method overriding, each class performs its own work and then uses super() to pass control to the next implementation in the MRO.
Example:
class Root:
def process(self):
return ["Root"]
class Left(Root):
def process(self):
return ["Left"] + super().process()
class Right(Root):
def process(self):
return ["Right"] + super().process()
class Child(Left, Right):
def process(self):
return ["Child"] + super().process()
Child().process() follows the MRO and returns entries from Child, Left, Right, and Root once each.
For this pattern to work correctly:
- Every participating implementation should call
super(). - Method signatures must be mutually compatible.
- Each method should consume its own arguments and forward the remaining arguments when necessary.
- A terminating implementation must exist.
Directly calling a named parent can break this chain.
How do abstract methods relate to method overriding? Explain using the abc module.
An abstract method declares behavior that subclasses are expected to implement through method overriding. Python provides abstract base classes through the abc module.
Example:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, length, breadth):
self.length = length
self.breadth = breadth
def area(self):
return self.length * self.breadth
Shape cannot normally be instantiated because area() is abstract. Rectangle becomes concrete by overriding area().
Abstract methods:
- Define a required interface.
- Improve consistency among subclasses.
- Allow polymorphic use through a common base class.
- May contain an implementation that an overriding method can call with
super().
If a subclass fails to implement all required abstract methods, that subclass also remains abstract.
Compare method overriding with simply adding a new method to a subclass. Discuss interface compatibility.
When a subclass overrides a method, it replaces an inherited implementation while preserving the conceptual operation represented by that method. Adding a new method introduces behavior that was not part of the inherited interface.
Example:
- Overriding
draw()inCirclechanges how a generalShapeis drawn. - Adding
calculate_diameter()toCircleintroduces circle-specific behavior.
A good override should honor the base method's contract:
- Accept calls that validly target the base method.
- Return an appropriate kind of result.
- Preserve documented expectations and invariants.
- Avoid imposing surprising new requirements.
This compatibility supports substitutability: code written for the base class should continue to work when given a subclass object. An unrelated new method does not affect calls through the base interface and is available only to code aware of the subclass capability.
Develop a Python example that combines method overriding and operator overloading, and explain how both forms of polymorphism operate.
The following example combines overridden methods with an overloaded operator:
class Shape:
def area(self):
raise NotImplementedError
def __lt__(self, other):
if not isinstance(other, Shape):
return NotImplemented
return self.area() < other.area()
class Rectangle(Shape):
def __init__(self, length, breadth):
self.length = length
self.breadth = breadth
def area(self):
return self.length * self.breadth
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
rectangle = Rectangle(4, 5)
circle = Circle(3)
result = rectangle < circle
Method overriding: Rectangle and Circle provide different implementations of area(). The implementation is selected according to the runtime type of each object.
Operator overloading: Shape.__lt__() defines the meaning of < for shapes. It invokes the overridden area() methods and compares their results.
This design demonstrates runtime polymorphism: one shared comparison implementation works with multiple subclasses because each subclass supplies its own area calculation.
Define function overloading. Does Python support traditional function overloading based on parameter types or parameter count?
Function overloading means defining multiple functions with the same name but different parameter lists so that the appropriate version is selected based on the arguments.
Python does not support traditional compile-time function overloading. If multiple functions with the same name are defined in the same scope, the latest definition replaces the earlier definitions.
For example:
def display(value):
return str(value)
def display(value, prefix):
return prefix + str(value)
Only the second definition remains available. Calling display(10) therefore raises a TypeError.
Python achieves similar behavior using:
- Default arguments
- Variable-length arguments such as
*argsand**kwargs - Type inspection
functools.singledispatch
Thus, Python provides flexible alternatives rather than traditional signature-based overloading.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →