Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Python

Python vs C++ Series: Polymorphism and Duck Typing

5.00/5 (6 votes)
11 Oct 2021CPOL6 min read 8.9K  
Introduce Python's way to support polymorphism and duck typing from the concept of C++ polymorphism
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++.

C++
#include <string>

void myOverloadingFunction(int parameter)
{
    // Do something
}

void myOverloadingFunction(std::string parameter)
{
    // Do something
}

void myOverloadingFunction(int parameter1, std::string parameter2, float parameter3)
{
    // Do something
}

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++.

C++
#include <memory>

class BaseClass
{
    public:
        virtual void doWork()
        {
            // do some work
        }
};

class DerivedClassA: public BaseClass
{
    public:
        virtual void doWork() override
        {
            // do some work
        }
};

class DerivedClassB: public BaseClass
{
    public:
        virtual void doWork() override
        {
            // do some work
        }
};

void myFunction(std::shared_ptr<BaseClass> p)
{
    // The appropriate doWork() to be called will be determined by
    // the instance of p at the runtime.
    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:

C++
class MyInterface
{
    // Since this class contains a pure virtual class; it becomes an abstract
    // class, and cannot be instantiated.
    public:
        // Use a pure virtual function to define an interface.
        virtual int method(int parameter) = 0;
};

class DerivedClass: public MyInterface
{
    public:
        // If the derived class needs to be instantiated, the derived class
        // must implement its parent's pure virtual function.
        virtual int method(int parameter) override
        {
            // do something
        }
};

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.

Python
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__":
    # Valid
    my_function(10)
    my_function("hello")

    # TypeError exception will be thrown
    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:

Bash
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.

Python
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:

Bash
10 
2.3

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.

Python
def my_function(parameter1, parameter2=None, parameter3="hello"):
    print(parameter1)
    if parameter2:
        print(parameter2)
    print(parameter3)

if __name__ == "__main__":
    # Use default parameter2 and parameter3; parameter 1 does not
    # have default value, so it cannot be omitted.
    my_function(10)

    # Use default parameter2
    my_function(parameter1=1, parameter3=5)

    # Use default parameter3; also notice that the order does not matter
    # when using keyword arguments.
    my_function(parameter2="world", parameter1=1)

The output of my_function(10):

Bash
10
hello

The output of my_function(parameter1=1, parameter3=5):

1
5

The output of my_function(parameter2=”world”, parameter1=1):

1
world
hello

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.)

Python
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.

Python
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.)

Python
$ 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 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:

  1. 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+.
  2. 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++):

Python
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.

Python
class AVLTree(BasicBinaryTree):
    """AVL Tree implementation."""

    def insert(self, key: int) -> None:
        # The AVL Tree implementation
        pass

    def delete(self, key: int) -> None:
        # The AVL Tree implementation
        pass

    def search(self, key: int) -> AVLNode:
        # The AVL Tree implementation
        pass

The complete example of the binary tree interface is the following (also available at python_interface.py).

Python
import abc

from typing import Optional
from dataclasses import dataclass

@dataclass
class Node:
    """Basic binary tree node definition."""

    key: int
    left: Optional["Node"] = None
    right: Optional["Node"] = None
    parent: Optional["Node"] = None

class BasicBinaryTree(abc.ABC):
    """An abstract base class defines the interface for any type of binary trees.

    The derived class should implement the abstract method defined in the abstract
    base class.
    """

    @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):
    """Binary Search Tree."""

    def insert(self, key: int) -> None:
        # The BST implementation
        pass

    def delete(self, key: int) -> None:
        # The BST implementation
        pass

    def search(self, key: int) -> Node:
        # The BST implementation
        pass

@dataclass
class AVLNode(Node):
    """AVL Tree node definition. Derived from Node."""

    left: Optional["AVLNode"] = None
    right: Optional["AVLNode"] = None
    parent: Optional["AVLNode"] = None
    height: int = 0

class AVLTree(BasicBinaryTree):
    """AVL Tree implementation."""

    def insert(self, key: int) -> None:
        # The AVL Tree implementation
        pass

    def delete(self, key: int) -> None:
        # The AVL Tree implementation
        pass

    def search(self, key: int) -> AVLNode:
        # The AVL Tree implementation
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)