This second article of the Python vs. C++ series discusses polymorphism.
This is the second article of the Python vs C++ Series. In this article, we are going to talk about another basic object-oriented programming concept – Polymorphism.
(Note that the Python code in the series assumes Python 3.7 or newer.)
Brief Review of Polymorphism
Polymorphism is a Greek word that means having many forms. A programming language that supports polymorphism means a variable, a function, or an object can take on multiple forms, such as a function accepting a parameter with different types. Also, with polymorphism, we can define functions with the same name (i.e., the same interface), but the functions have multiple implementations.
Polymorphism in C++
C++ supports static (resolved at compile-time) and dynamic (resolved at runtime) polymorphism.
Function Overloading
In C++, static polymorphism is also known as function overloading, allowing programs to declare multiple functions with the same name but different parameters. The following shows a sample of function overloading in C++.
#include <string>
void myOverloadingFunction(int parameter)
{
}
void myOverloadingFunction(std::string parameter)
{
}
void myOverloadingFunction(int parameter1, std::string parameter2, float parameter3)
{
}
Virtual Function and Abstract Class
The C++ implements runtime polymorphism is supported by using class hierarchy with virtual functions. When a method in a derived class overrides its base class, the method to call is determined by the object’s type at run time. The following code shows how it works in C++.
#include <memory>
class BaseClass
{
public:
virtual void doWork()
{
}
};
class DerivedClassA: public BaseClass
{
public:
virtual void doWork() override
{
}
};
class DerivedClassB: public BaseClass
{
public:
virtual void doWork() override
{
}
};
void myFunction(std::shared_ptr<BaseClass> p)
{
p->doWork();
}
Interface and Pure Virtual Function
When a virtual function appends with = 0, it becomes a pure virtual function, and a class that contains a pure virtual function is called an abstract class. Any class derived from an abstract class must define its pure virtual functions if it’s supported to be instantiated. Therefore, we usually use an abstract class to define programs’ interfaces. For example:
class MyInterface
{
public:
virtual int method(int parameter) = 0;
};
class DerivedClass: public MyInterface
{
public:
virtual int method(int parameter) override
{
}
};
Duck Typing in Python
Duck typing is an application of duck test which says, “If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.” With duck typing, a function does not check the parameter’s type; instead, it checks the presence of the parameter. Duck typing is the essence of dynamic languages like Python.
Function Overloading
One reason we need polymorphism in C++ is that C++ is a static language. Polymorphism allows us to define functions with the same name but take different type parameters or different numbers of parameters. On the contrary, Python is a dynamic language. Therefore, function overloading is not necessary for Python and is not supported (see PEP3142). The following example shows how a Python function deals with a parameter that could be of different types.
def my_function(parameter):
if type(parameter) is str:
print("Do something when the type is string")
elif type(parameter) is int:
print("Do something when the type is integer")
else:
raise TypeError("Invalid type")
if __name__ == "__main__":
my_function(10)
my_function("hello")
my_function(2.3)
In this example, my_function
can take any parameter due to the nature of duck typing. Therefore, if we want the function to perform different operations based on the parameter’s type, the function needs to check the presence of the parameter to determine what to do. The output of this example looks like the following:
Do something when the type is integer
Do something when the type is string
Traceback (most recent call last):
….
raise TypeError("Invalid type")
TypeError: Invalid type
What will happen if we define multiple functions with the same name?
If we define two or more functions with the same name, the Python interpreter will use the last one it scans. For example, the following code will work, but only the last my_function
will be used.
def my_function(parameter):
if type(parameter) is str:
print("Do something when the type is string")
elif type(parameter) is int:
print("Do something when the type is integer")
else:
raise TypeError("Invalid type")
def my_function(parameter):
print(parameter)
if __name__ == "__main__":
my_function(10)
my_function(2.3)
And its output looks like below:
The last my_function(parameter)
is the one that is really called, and that’s why my_function(2.3)
works.
Function with Optional Parameters
In addition to defining functions with the same name but with different parameter types, with polymorphism, we can also define functions with the same name but take a different number of parameters. Python does not support function overloading, but its keyword arguments and default arguments abilities provide a way to define a function that accepts a different number of parameters. The following code snippet demonstrates the usage of keyword arguments and default arguments.
def my_function(parameter1, parameter2=None, parameter3="hello"):
print(parameter1)
if parameter2:
print(parameter2)
print(parameter3)
if __name__ == "__main__":
my_function(10)
my_function(parameter1=1, parameter3=5)
my_function(parameter2="world", parameter1=1)
The output of my_function(10)
:
The output of my_function(parameter1=1, parameter3=5)
:
The output of my_function(parameter2=”world”, parameter1=1)
:
The Disadvantage When Using Duck Typing
With duck typing, Python allows us to write a function with different types and a different number of parameters. However, when we need our function to perform certain operations based on the parameters, we will need several if
-else
statements for each type, and if the if
-else
statement is getting longer, its readability reduces. When this happens, we may need to consider refactoring our code. Python does not have the function overloading benefit that we can leverage function overloading to perform different actions using several small functions as we do in C++.
Type Safety and Type Checking
Although Python is a dynamic language, it does not mean Python does not care about type safety. Unlike C++ that a compiler will catch most of the type-related errors, Python relies on linting tools to do the job.
Starting from Python 3.5, PEP484 introduces the support of type hints that type checking tools such as mypy can leverage to check Python programs. The following example shows an example of type hints.
(More detail of Python’s type hints can be found at Support for type hints.)
from typing import Dict, Optional, Union
class MyClass:
def my_method_1(self, parameter1: int, parameter2: str) -> None:
pass
def my_method_2(self, parameter: Union[int, str]) -> Dict:
pass
def my_function(parameter: Optional[MyClass]):
pass
Although we can use type hints and linting tools to ensure type safety, the Python runtime does not enforce functions or variables to satisfy its type annotations. If we ignore errors or warnings generated by a type checker and still pass an invalid type parameter, the Python interpreter will still execute it.
For example, my_function
in the example below expects that parameter1
is int type and parameter2 is string type. However, when the function is called and the type of parameter1 is string and the parameter2 is float, the Python interpreter will execute it.
def my_function(parameter1: int, parameter2: str) -> None:
print(f"Parameter 1: {parameter1}")
print(f"Parameter 2: {parameter2}")
if __name__ == "__main__":
my_function(parameter1="Hello", parameter2=3.5)
If we run a type checker (using mypy
in the example), it will show incompatible type errors. (Use mypy
as an example.)
$ mypy python_type_hints_2.py
python_type_hints_2.py:12: error: Argument "parameter1" to "my_function" has incompatible type "str"; expected "int"
python_type_hints_2.py:12: error: Argument "parameter2" to "my_function" has incompatible type "float"; expected "str"
Found 2 errors in 1 file (checked 1 source file)
However, if we execute the code, it will still work.
$ python python_type_hints_2.py
Parameter 1: Hello
Parameter 2: 3.5
Python type hints are only for linting tools to check types but have no effect on runtime.
Abstract Base Class and Interface
Python does not have the concept of virtual function or interface like C++. However, Python provides an infrastructure that allows us to build something resembling interfaces’ functionality – Abstract Base Classes (ABC).
ABC is a class that does not need to provide a concrete implementation but is used for two purposes:
- Check for implicit interface compatibility. ABC defines a blueprint of a class that may be used to check against type compatibility. The concept is similar to the concept of abstract classes and virtual functions in C+.
- Check for implementation completeness. ABC defines a set of methods and properties that a derived class must implement.
In the following example, BasicBinaryTree
defines the interface for binary trees. Any derived binary tree (e.g., an AVL tree or a Binary Search Tree) must implement the methods defined in the BasicBinaryTree
.
To use ABC to define an interface, the interface class needs to inherit from the helper class – abc.ABC. The method that the derived classes must implement uses @abc.abstractmethod decorator (equivalent to pure virtual functions in C++):
class BasicBinaryTree(abc.ABC):
@abc.abstractmethod
def insert(self, key: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def delete(self, key: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def search(self, key: int) -> Node:
raise NotImplementedError()
The derived class (use AVLTree
as an example) inherits the abstract base class (i.e., BasicBinaryTree
in this example) and implements the methods defined with the @abc.abstactmethod decorator.
class AVLTree(BasicBinaryTree)
def insert(self, key: int) -> None:
pass
def delete(self, key: int) -> None:
pass
def search(self, key: int) -> AVLNode:
pass
The complete example of the binary tree interface is the following (also available at python_interface.py).
import abc
from typing import Optional
from dataclasses import dataclass
@dataclass
class Node
key: int
left: Optional["Node"] = None
right: Optional["Node"] = None
parent: Optional["Node"] = None
class BasicBinaryTree(abc.ABC)
@abc.abstractmethod
def insert(self, key: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def delete(self, key: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def search(self, key: int) -> Node:
raise NotImplementedError()
class BinarySearchTree(BasicBinaryTree)
def insert(self, key: int) -> None:
pass
def delete(self, key: int) -> None:
pass
def search(self, key: int) -> Node:
pass
@dataclass
class AVLNode(Node)
left: Optional["AVLNode"] = None
right: Optional["AVLNode"] = None
parent: Optional["AVLNode"] = None
height: int = 0
class AVLTree(BasicBinaryTree)
def insert(self, key: int) -> None:
pass
def delete(self, key: int) -> None:
pass
def search(self, key: int) -> AVLNode:
pass
Conclusion
Python may support polymorphism differently from C++, but the concept of polymorphism is still valid and widely used in Python programs, and the importance of type safety is still essential to Python programs.