This lesson is still being designed and assembled (Pre-Alpha version)

# Polymorphism

## Overview

Teaching: 0 min
Exercises: 0 min
Questions
• What is Polymorphism?

Objectives
• Understand the concepts behind Polymorphism.

## Polymorphism

Polymorphism the concept of using different classes in place of one another. Specifically, an object is polymorphic if it can be used in place of one or more classes or interfaces. The intended use of a polymorphic object is to allow objects that are children of a parent class to be used as their parent class or for multiple objects that inherit from an interface to be used as the interface.

This is very similar to python’s concept of `Duck Typing`, i.e. if it looks like a duck and quacks like a duck, then it must be a duck. Duck typing is more of a passive concept, we assume objects of certain types can be used in certain ways and simply try to use them. For example, if we believe a variable is a number, we assume we can perform mathematical operations with it.

Polymorphism is the practice of making sure duck typing will work.

We will use the two classes, `Molecule` and `AtomMolecule` to create an example of polymorphism.

First we will restate the two classes here.

``````class Molecule:
def __init__(self, name, charge, symbols, coordinates):
self.name = name
self.charge = charge
self.symbols = symbols
self.coordinates = coordinates

@property
def symbols(self):
return self._symbols

@symbols.setter
def symbols(self, symbols):
self._symbols = symbols
self._update_num_atoms()

def _update_num_atoms(self):
self.num_atoms = len(self.symbols)

def __str__(self):
return f'name: {self.name}\ncharge: {self.charge}\nsymbols: {self.symbols}\ncoordinates: {self.coordinates}\nnumber of atoms: {self.num_atoms}'
``````
``````class AtomMolecule(Molecule):
def __init__(self, name, charge, atoms):
self.atoms = atoms
super().__init__(name, charge, self.symbols, self.coordinates)

@property
def atoms(self):
return self._atoms

@atoms.setter
def atoms(self, atoms):
self._atoms = atoms
self._update_symbols()
self._update_coordinates()

def _update_symbols(self):
list_symbols = []
for atom in self.atoms:
list_symbols.append(atom.symbol)
self._symbols = list_symbols

def _update_coordinates(self):
list_coordinates = []
for atom in self.atoms:
list_coordinates.append(atom.coordinates)
self.coordinates = list_coordinates
``````

Since `AtomMolecule` is a child of the `Molecule` class, take note that they both share a many variable names.. To ensure that `AtomMolecule` is polymorphic, we want to ensure that any method that operates on a `Molecule` will correctly operate on an instance of `AtomMolecule` as well.

Here we will build a simple method that utilizes variables of a `Molecule` to provide a formatted output.

``````def formatted_print(molecule):
return f'{molecule.name} is made of {molecule.symbols} and has an atomic charge of {molecule.charge}'
``````

We will create a `Molecule` to provide to the method.

``````mol1 = Molecule(name='water molecule', charge=0.0, symbols=["O", "H", "H"], coordinates=[[0,0,0],[0,1,0],[0,0,1]])
formatted_print(mol1)
``````
``````"water molecule is made of ['O', 'H', 'H'] and has an atomic charge of 0.0"
``````

For `AtomMolecule` to be polymorphic, it should also work with the method.

``````oxygen = Atom("oxygen", "O", 8, 15.999, [0,0,0])
hydrogen1 = Atom("hydrogen", "H", 1, 1.00784, [0,1,0])
hydrogen2 = Atom("hydrogen", "H", 1, 1.00784, [0,0,1])

mol2 = AtomMolecule(name='water molecule', charge=0.0, atoms=[oxygen, hydrogen1, hydrogen2])
formatted_print(mol2)
``````
``````"water molecule is made of ['O', 'H', 'H'] and has an atomic charge of 0.0"
``````

We have properly made `AtomMolecule` polymorphic. Any method that utilizes `Molecule` objects should be able to use `AtomMolecule` objects. This allows us to extend the behaviour of a `Molecule` without breaking any code that relies on it.

## Key Points

• Encapsulation

• Inheritance