CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Systèmes d'Information et Programmation - TD : programmation orientée objet

Présentation

Nous allons nous intéresser à la modélisation de réactions chimiques.

Par exemple, en présence de molécules de méthane et de dioxygène, une réaction de combustion va produire du dioxyde de carbone et de l'eau :

CH4 + O2 + O2 → CO2 + H2O + H2O

Quelques rappels :

  • Un atome est constitué de nucléons (les protons et les neutrons) et d'électrons. Pour cet exercice, nous ignorerons les électrons.
    Le nombre de masse d'un atome est son nombre de nucléons.
  • Les atomes ayant le même nombre de protons sont classés par un élément chimique : les atomes d'hydrogène
    ont un proton, ceux d'oxygène huit. Le nombre de protons est nommé numéro atomique.
  • Les atomes caractérisés par un même élément chimique peuvent différer par leur nombre de neutrons, ce sont des isotopes.
    Par exemple, il existe 3 isotopes de l'hydrogène : le protium n'a pas de neutron, le deutérium a un neutron
    et le tritium a 2 neutrons.
  • Une molécule est un ensemble d'atomes (nous ne tiendrons pas compte des isomères).

Composant chimique

Nous nommerons composant chimique les entités que nous allons représenter, à savoir les atomes et les molécules. Un composant chimique a un nom est une masse (la masse d'un composant chimique sera le nombre de masse pour un atome, la somme des nombres de masse des atomes la constituant pour une molécule).

  • Définir la classe ChemicalComponent ; cette classe étant abstraite, elle sera une sous-classe de la classe ABC du module abc.
  • Définir le constructeur de cette classe, il recevra le nom comme argument. Ce nom ne sera pas modifiable après l'initialisation, donc il sera mémorisé dans un attribute privé (commençant par __), et accessible comme une propriété (décorateur @property sur l'accesseur).
  • Définir une méthode abstraite (décorateur @abstractmethod) mass().
  • Essayer de créer une instance de ChemicalComponent, vérifier qu'une erreur est déclenchée.

ANSWER ELEMENTS

from abc import ABC, abstractmethod

class ChemicalComponent(ABC):
    def __init__(self, name):
        self.__name = name

    @property
    def name(self):
        return self.__name

    @abstractmethod
    def mass(self):
        pass

#chemical_component = ChemicalComponent("Test ChemicalComponent")

Élément chimique

Un élément chimique est un composant chimique constitué d'un seul atome dont on connaît le nombre de protons.

Nous pourrions mémoriser ce nombre dans un attribut non modifiable avec la même approche que celle retenue pour le nom. Cependant, pour rendre plus facile l'utilisation de notre code, nous allons proposer directement la création d'un atome d'hydrogène, plutôt que la création d'un atome avec un seul proton. Ce qui revient à dire que nous aurons une classe Hydrogen (même chose pour les autres éléments chimiques). Le nombre de protons sera une caractéristique de cette classe, et non pas d'une instance particulière de celle-ci (tous les atomes d'hydrogène ont un proton). Un attribut de classe ne peut pas être redéfini dans une sous-classe, contrairement à une méthode de classe, c'est donc la solution que nous utiliserons.

Un élément chimique définit aussi un symbole (H pour l'hydrogène, O pour oxygène…), nous utiliserons la même approche pour cette caractéristique.

  • Définir la classe ChemicalElement ; cette classe sera aussi abstraite, sous-classe de ChemicalComponent.
  • Définir le constructeur de cette classe, il recevra le nom comme argument.
  • Définir les méthodes de classe (décorateur @classmethod) atomic_number() et symbol().
  • Vérifier qu'il n'est toujours pas possible de créer une instance de cette classe.

ANSWER ELEMENTS

class ChemicalElement(ChemicalComponent):
    def __init__(self, name):
        super().__init__(name)

    @classmethod
    def symbol(cls):
        pass

    @classmethod
    def atomic_number(cls):
        pass

#chemical_element = ChemicalElement("Test ChemicalElement")

Atome

Un atome est un élément chimique dont on connaît le nombre de neutrons. Ce nombre sera non modifiable après son initialisation, mémorisé dans un attribut privé.

  • Définir la classe Atom ; cette classe sera une sous-classe de la classe ChemicalElement.
  • Définir le constructeur de cette classe, il recevra comme arguments le nom et le nombre de neutrons.
  • Définir la méthode neutron_number() et redéfinir la méthode mass() avec la bonne sémantique.
  • Vérifier qu'il est maintenant possible de créer une instance de cette classe (exemple d'instance : deutérium), afficher son nom, son nombre de neutrons.
  • Pour autant, cette classe n'est pas destinée à être instanciée directement, car son nombre de protons n'est pas encore connu : vérifier ceci en essayant d'afficher la masse de l'atome que vous avez créé.

ANSWER ELEMENTS

class Atom(ChemicalElement):
    def __init__(self, name, neutron_number):
        super().__init__(name)
        self.__neutron_number = neutron_number

    def neutron_number(self):
        return self.__neutron_number

    def mass(self):
        return self.atomic_number() + self.neutron_number()

deuterium = Atom("Deuterium", 1)
print(deuterium.name)
print(deuterium.neutron_number())
#print(deuterium.mass())

Hydrogène

L'hydrogène est l'élément chimique ayant 1 comme numéro atomique et H comme symbole.

Il existe trois isotopes de l'hydrogène : le protium n'a pas de neutron (99,99% de l'hydrogène présent sur la Terre), le deutérium en a un (0,01%) et le tritium deux (n'existe pas à l'état naturel, instable).

  • Définir la classe Hydrogen ; cette classe sera une sous-classe de la classe Atom.
  • Définir le constructeur de cette classe, il recevra comme arguments le nom (qui sera par défaut Hydrogen) et le nombre de neutrons (0 par défaut). Si ce dernier n'est pas valide, une exception TypeError sera émise.
  • Redéfinir les méthodes atomic_number() et symbol() avec la bonne sémantique.
  • Créer un atome de deutérium et afficher sa masse.
  • Afficher l'atome de deutérium créé. Le résultat qui ressemble à
  <__main__.Hydrogen object at 0x1043e6390>

n'est pas satisfaisant.

Nous n'allons pas essayer de l'afficher sous la forme officielle (2H), nous retiendrons l'écriture [1/2]H : le premier nombre est le numéro atomique, le second est le nombre de masse, et la lettre est le symbole.

Quand Python veut convertir une donnée en chaîne de caractères pour l'afficher, il va utiliser la méthode __str__() qui a une définition par défaut dans la classe object, la racine de l'arbre d'héritage. Nous allons donc redéfinir cette méthode pour obtenir l'affichage souhaité.

  • Dans quelle classe est-il pertinent de redéfinir cette méthode ? Ajouter cette redéfinition.

ANSWER ELEMENTS

# Method added in class Atom
    def __str__(self):
        return f"[{self.atomic_number()}/{self.mass()}]{self.symbol()}"

class Hydrogen(Atom):
    def __init__(self, name = 'Hydrogen', neutron_number = 0):
        if not 0 <= neutron_number <= 2:
            raise TypeError("Invalid neutron number in Hydrogen")
        super().__init__(name, neutron_number)

    @classmethod
    def atomic_number(cls):
        return 1

    @classmethod
    def symbol(cls):
        return 'H'

deuterium = Hydrogen("Deuterium", 1)
print(deuterium.mass())

Autres atomes

Pour la réaction chimique que nous souhaitons modéliser, nous avons besoin des atomes de carbone (symbole C, 6 protons, 6 à 8 neutrons, l'isotope le plus courant est 12C) et d'oxygène (symbole O, 8 protons, 6 à 20 neutrons, l'isotope le plus courant est 160).

  • Définir les classes Carbon et Oxygen.

ANSWER ELEMENTS

class Carbon(Atom):
    def __init__(self, name = 'Carbon', neutron_number = 6):
        if not 6 <= neutron_number <= 8:
            raise TypeError("Invalid neutron number in Carbon")
        super().__init__(name, neutron_number)

    @classmethod
    def atomic_number(cls):
        return 6

    @classmethod
    def symbol(cls):
        return 'C'

class Oxygen(Atom):
    def __init__(self, name = 'Oxygen', neutron_number = 8):
        if not 6 <= neutron_number <= 20:
            raise TypeError("Invalid neutron number in Oxygen")
        super().__init__(name, neutron_number)

    @classmethod
    def atomic_number(cls):
        return 8

    @classmethod
    def symbol(cls):
        return 'O'

Molécule

Une molécule est un composant chimique constitué d'atomes, au moins deux.

  • Définir la classe Molecule, sous-classe de ChemicalComponent.
  • Définir le constructeur de cette classe, il recevra un nom et la liste d'atomes comme arguments, celle-ci sera mémorisée dans un attribut privé. Le constructeur vérifiera que la liste contient bien uniquement des atomes (sinon une exception TypeError sera émise) ; pour cela, il est possible d'utiliser la function
isinstance(une_instance, UneClass)

qui teste si une_instance est une instance de la classe UneClasse ou de l'une de ses sous-classes.

  • Redéfinir la méthode mass() avec la bonne sémantique.
  • Créer une molécule d'eau, et afficher sa masse.
  • Afficher cette molécule, le résultat n'est pas satisfaisant.

L'objectif est d'obtenir un affichage sous la forme :

  H2O

c'est à dire afficher chaque type d'atome de la molécule, suivi s'il y en a plusieurs de ce type du nombre d'occurence. On ne cherchera pas à respecter un ordre pour les types d'atomes.

  • Définir dans la classe Molecule une méthode retournant un dictionnaire décrivant la composition d'une molécule en terme de types d'atomes.
  • Redéfinir la méthode __str__() de cette classe pour obtenir l'affichage souhaité.

Il est possible d'utiliser les classes Python comme clefs du dictionnaire.

La classe d'un objet est obtenue par la fonction type().


ANSWER ELEMENTS

class Molecule(ChemicalComponent):
    def __init__(self, name, atoms):
        for atom in atoms:
            if not isinstance(atom, Atom):
                raise TypeError("Not an Atom")
        super().__init__(name)
        self.__atoms = atoms

    def mass(self):
        return sum([atom.mass() for atom in self.__atoms])

    def composition(self):
        d = {}
        for atom in self.__atoms:
            d[type(atom)] = d.get(type(atom), 0) + 1
        return d

    def __str__(self):
        return "".join([f"{a.symbol()}{'' if n == 1 else n}" for a, n in self.composition().items()])

water = Molecule("Water", [Hydrogen(), Hydrogen(), Oxygen()])
print(water.mass())
print(water)

Eau et autres molécules

Nous avons besoin des sortes de molécules suivantes : méthane, dioxygène, dioxyde de carbone, eau.

  • Définir les classe Methane, Dioxygen, CarbonDioxide et Water, sous-classes de Molecule.
  • Définir les constructeurs de ces classes, ils recevront un nom, avec le nom de la classe comme valeur par défaut, et la liste d'atomes comme arguments ; la valeur par défaut de la liste sera une liste des atomes constituant ces molécules. Le constructeur vérifiera que la liste contient bien uniquement les bons atomes, une exception TypeError sera émise dans le cas contraire.

ANSWER ELEMENTS

class Methane(Molecule):
    def __init__(self, name = "Methane", atoms = [Carbon(), Hydrogen(), Hydrogen(), Hydrogen(), Hydrogen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 2:
            raise TypeError("Bad atoms in methane")
        if not Hydrogen in d.keys() or d[Hydrogen] != 4:
            raise TypeError("Methane needs 4 atoms of Hydrogen")
        if not Carbon in d.keys() or d[Carbon] != 1:
            raise TypeError("Methane needs 1 atom of Carbon")

class Dioxygen(Molecule):
    def __init__(self, name = "Dioxygen", atoms = [Oxygen(), Oxygen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 1:
            raise TypeError("Bad atoms in dioxigen")
        if not Oxygen in d.keys() or d[Oxygen] != 2:
            raise TypeError("Dioxygen needs 2 atoms of Oxygen")

class CarbonDioxide(Molecule):
    def __init__(self, name = "CarbonDioxide", atoms = [Carbon(), Oxygen(), Oxygen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 2:
            raise TypeError("Bad atoms in carbon dioxide")
        if not Oxygen in d.keys() or d[Oxygen] != 2:
            raise TypeError("Carbon dioxide needs 2 atoms of Oxygen")
        if not Carbon in d.keys() or d[Carbon] != 1:
            raise TypeError("Carbon dioxide needs 1 atom of Carbon")

class Water(Molecule):
    def __init__(self, name = "Water", atoms = [Hydrogen(), Hydrogen(), Oxygen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 2:
            raise TypeError("Bad atoms in water")
        if not Hydrogen in d.keys() or d[Hydrogen] != 2:
            raise TypeError("Water needs 2 atoms of Hydrogen")
        if not Oxygen in d.keys() or d[Oxygen] != 1:
            raise TypeError("Water needs 1 atom of Oxygen")

Réaction chimique

Nous modéliserons une réaction chimiques sous la forme d'une fonction ChemicalReaction.

  • Cette fonction recevra une liste de molécules.
  • Elle cherchera dans cette liste si elle trouve des molécules permettant de provoquer une réaction chimique (nous nous limiterons à la réaction chimique de combustion mentionnée au début du sujet).
  • Si c'est le cas, elle retirera de la liste ces molécules, exécutera la réaction et retournera la liste des molécules produites. Dans le cas contraire, elle retournera None. Les molécules créées seront construites à partir des atomes des molécules à l'origine de la réaction chimique.

Nous savons comment créer les molécules produites par la réaction chimique, grace aux constructeurs, mais nous n'avons pas pour l'instant de moyen de récupérer les atomes des molécules à l'origine de la réaction chimique.

  • Ajouter dans la classe Molecule une méthode destroy() qui vide la molécule de ses atomes et retourne une liste de ceux-ci.

ANSWER ELEMENTS

# Method added in class Molecule
    def destroy(self):
        atoms = self.__atoms
        self.__atoms = []
        return atoms

def ChemicalReaction(molecules):
    def count_molecules(kind):
        return sum([1 for m in molecules if isinstance(m, kind)])

    def remove_molecule(kind):
        for i in range(0, len(molecules)):
            if isinstance(molecules[i], kind):
                return molecules.pop(i)
        return None

    def remove_atom(atoms, kind):
        for i in range(0, len(atoms)):
            if isinstance(atoms[i], kind):
                return atoms.pop(i)
        return None


    dioxygen_count = count_molecules(Dioxygen)
    methane_count = count_molecules(Methane)

    if methane_count >= 1 and dioxygen_count >= 2:
        needed_molecules = [remove_molecule(Methane), remove_molecule(Dioxygen), remove_molecule(Dioxygen)]
        all_atoms = sum([m.destroy() for m in needed_molecules], [])
        return [
            Water        ("w1", [remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Oxygen)]),
            Water        ("w2", [remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Oxygen)]),
            CarbonDioxide("c",  [remove_atom(all_atoms, Carbon),   remove_atom(all_atoms, Oxygen),   remove_atom(all_atoms, Oxygen)])
        ]
    return None