Python FAQ: Top Questions
25. What is the difference between an `abstract method` and a `virtual method` in Python?
The concepts of "abstract method" and "virtual method" are related to object-oriented programming and polymorphism. While Python doesn't use the exact terms "virtual method" as explicitly as languages like C++ or Java, it does have a clear concept of abstract methods and its default method behavior aligns with what's typically meant by "virtual."
1. Abstract Method:
- Concept: An **abstract method** is a method declared in an abstract base class (ABC) that has a declaration but no implementation. It serves as a placeholder for a method that *must* be implemented by concrete (non-abstract) subclasses.
-
Purpose:
- To enforce a contract: Abstract methods define an interface or a set of behaviors that any concrete subclass must provide.
- To prevent instantiation of incomplete classes: An abstract class cannot be instantiated directly if it contains unimplemented abstract methods. Subclasses must implement all abstract methods to become concrete and instantiable.
-
Implementation in Python:
- Python achieves abstract methods using the `abc` (Abstract Base Classes) module.
- You define an abstract class by inheriting from `ABC` and decorate abstract methods with `@abstractmethod`.
- Example Use: Think of a `Shape` abstract class with an `area()` abstract method. All concrete shapes (Circle, Square) *must* implement their own `area()` calculation.
2. Virtual Method (Python's Default Behavior):
- Concept: In languages like C++ or Java, a **virtual method** is a method in a base class that can be overridden by methods in derived classes. The key aspect is **dynamic dispatch** (or late binding): when a virtual method is called on an object through a pointer or reference to a base class, the runtime system determines which specific implementation (base class or derived class) to call based on the actual type of the object, not the type of the pointer/reference.
-
Python's Approach:
- **All methods in Python are inherently "virtual" by default.** Python's method dispatch mechanism is always dynamic. When you call `obj.method()`, Python looks up `method` in `obj`'s class, then its parent classes in the MRO, and executes the first one it finds. This means that if a subclass defines a method with the same name as a superclass method, the subclass's method will always override and be called for instances of that subclass.
- You don't need a special keyword like `virtual` (as in C++) or `override` (as in Java) to make a method overrideable. It's the default behavior.
- Purpose: To enable **polymorphism**, where objects of different classes can be treated as objects of a common base type, yet exhibit their specific behaviors.
Summary Table:
Feature | Abstract Method | "Virtual Method" (Python's Default) |
---|---|---|
Definition | Declared in ABC, no implementation (placeholder). Must be overridden. | Any method in a base class that can be overridden by a subclass. |
Implementation | Must be implemented by concrete subclasses. | Has an implementation in the base class, can be overridden by subclass. |
Enforcement | Strictly enforced by `abc` module: subclass *must* implement or be abstract. | No explicit enforcement; overriding is optional. |
Instantiability of Base Class | Cannot be instantiated if it has unimplemented abstract methods. | Can be instantiated, and its methods will be called unless overridden. |
Keywords | `from abc import ABC, abstractmethod`, `@abstractmethod` | None (it's Python's default method behavior) |
Core Principle | Defines an interface/contract. | Enables dynamic dispatch and polymorphism. |
In essence, an **abstract method** dictates *what* a subclass *must* do, providing a blueprint. Python's default method behavior, akin to **virtual methods**, means *how* a subclass *chooses* to implement or override a method will be dynamically selected at runtime based on the object's actual type, enabling flexible polymorphic behavior.
import abc
# --- Example 1: Abstract Method ---
print("--- Abstract Method Example ---")
# Define an Abstract Base Class (ABC)
class Shape(abc.ABC): # Inherit from abc.ABC
def __init__(self, name):
self.name = name
# This is an abstract method. It has no implementation here.
# Any concrete subclass of Shape MUST implement 'area()'.
@abc.abstractmethod
def area(self):
pass # No implementation
# This is a regular (virtual) method that can be inherited or overridden
def describe(self):
return f"This is a {self.name}."
# try:
# # Cannot instantiate an abstract class with unimplemented abstract methods
# abstract_shape = Shape("Generic")
# except TypeError as e:
# print(f"Cannot instantiate Shape directly: {e}")
class Circle(Shape):
def __init__(self, radius):
super().__init__("Circle")
self.radius = radius
# Must implement the abstract 'area' method
def area(self):
return 3.14159 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("Rectangle")
self.width = width
self.height = height
# Must implement the abstract 'area' method
def area(self):
return self.width * self.height
# Class AttemptToBreakContract does not implement 'area()'
# class AttemptToBreakContract(Shape):
# def __init__(self, name):
# super().__init__(name)
#
# try:
# invalid_instance = AttemptToBreakContract("Broken")
# except TypeError as e:
# print(f"\nCaught expected TypeError for un-implemented abstract method: {e}")
# Create instances of concrete subclasses
circle = Circle(5)
rectangle = Rectangle(4, 6)
print(f"{circle.describe()} Area: {circle.area():.2f}")
print(f"{rectangle.describe()} Area: {rectangle.area():.2f}")
# --- Example 2: "Virtual Method" (Python's default behavior) ---
print("\n--- 'Virtual Method' (Python's Default) Example ---")
class Vehicle:
def __init__(self, brand):
self.brand = brand
# This method is "virtual" because it can be overridden
def start_engine(self):
print(f"{self.brand} vehicle engine starts with a generic sound.")
def drive(self):
print(f"{self.brand} vehicle is driving.")
class Car(Vehicle):
def __init__(self, brand, model):
super().__init__(brand)
self.model = model
# Overriding the start_engine method
def start_engine(self):
print(f"{self.brand} {self.model}'s engine starts smoothly (Car specific).")
# Adding a new method specific to Car
def honk(self):
print(f"{self.brand} {self.model} goes Honk!")
class Bicycle(Vehicle):
def __init__(self, brand, type):
super().__init__(brand)
self.type = type
# Overriding the start_engine method to do nothing for a bike
def start_engine(self):
print(f"A {self.brand} {self.type} bicycle has no engine to start.")
# Create instances
generic_vehicle = Vehicle("GenericMotors")
my_car = Car("Toyota", "Camry")
my_bike = Bicycle("Giant", "Mountain Bike")
print("\n--- Calling Methods ---")
generic_vehicle.start_engine()
generic_vehicle.drive()
print("-" * 20)
my_car.start_engine() # Calls Car's overridden method
my_car.drive() # Inherits Vehicle's drive method
my_car.honk() # Car-specific method
print("-" * 20)
my_bike.start_engine() # Calls Bicycle's overridden method
my_bike.drive() # Inherits Vehicle's drive method
# Demonstrating Polymorphism
print("\n--- Polymorphism Example ---")
vehicles = [generic_vehicle, my_car, my_bike]
for v in vehicles:
v.start_engine() # Dynamic dispatch: Calls the correct overridden method
Explanation of the Example Code:
-
**Abstract Method Example (`Shape` ABC):**
- `Shape` is defined as an `abc.ABC` (Abstract Base Class).
- `area()` is decorated with `@abc.abstractmethod`. This signifies that any concrete subclass of `Shape` *must* provide an implementation for `area()`. If it doesn't (like the commented-out `AttemptToBreakContract`), Python will raise a `TypeError` when you try to instantiate that subclass, enforcing the contract.
- The commented-out line `abstract_shape = Shape("Generic")` also shows that you cannot directly instantiate `Shape` because it has an unimplemented abstract method.
- `Circle` and `Rectangle` successfully implement `area()`, allowing them to be instantiated and their specific area calculations to be performed.
-
**"Virtual Method" (Python's Default Behavior) Example (`Vehicle` and Subclasses):**
- The `Vehicle` class has a `start_engine()` method. This method is "virtual" by default in Python because it can be overridden by subclasses.
- `Car` and `Bicycle` subclasses both provide their own implementations of `start_engine()`. They don't need any special keyword (`virtual`, `override`) because Python's method lookup is dynamic.
- When `my_car.start_engine()` is called, Python finds and executes `Car`'s `start_engine()`. When `my_bike.start_engine()` is called, it executes `Bicycle`'s version.
- The `drive()` method in `Vehicle` is not overridden, so both `Car` and `Bicycle` instances inherit and use `Vehicle`'s `drive()` method.
- **Polymorphism Demonstration:** The `for v in vehicles:` loop iterates through a list containing instances of `Vehicle`, `Car`, and `Bicycle`. When `v.start_engine()` is called inside the loop, Python's dynamic dispatch ensures that the *correct* `start_engine()` method (from `Vehicle`, `Car`, or `Bicycle`) is executed based on the actual type of the object `v` at that moment. This is the essence of polymorphism.
These examples highlight that abstract methods define a mandatory interface that subclasses must fulfill, whereas Python's default method behavior (which is akin to "virtual" in other languages) enables flexible overriding and polymorphic behavior without special keywords.