1. Modules et classes

sept. 21, 2018

Objectifs

  • Comprendre l’intérêt de la programmation modulaire
  • Savoir concevoir et utiliser un module
  • Savoir définir de nouveaux types à l’aide de classes

1.1. Pourquoi la programmation modulaire ?

  • développement logiciel
  • modification et maintenance logicielle
  • réutilisabilité
  • création de nouveaux types de données

1.2. Les modules en Python

Module = fichier contenant un ensemble de déclarations de fonctions ou autres.

1.2.1. Importation d’un module

Plusieurs formes d’importation

  • from <module> import *
  • from <module> import <truc>
  • import <module>
  • import <module> as <autre nom>

La différence entre la forme from ... import ... et la forme import ... réside dans la forme à donner aux noms des fonctionnalités offertes par le module :

  • avec la première forme, il suffit d’invoquer le simple nom de la fonctionnalité ;
  • avec la seconde forme, il est nécessaire de pleinement qualifier le nom en le préfixant du nom du module.

Voici quelques exemples classiques :

  • importation de toutes les fonctions définies dans le module random

    >>> from random import *
    >>> randint(0, 10)
    5
    >>> l = [1, 2, 3]
    >>> shuffle(l)
    >>> l
    [3, 1, 2]
    
  • importation de deux définitions du module math

    >>> from math import cos, pi
    >>> pi
    3.141592653589793
    >>> cos(pi)
    -1.0
    
  • forme import math

    >>> import math
    >>> math.pi
    3.141592653589793
    >>> math.cos(math.pi)
    -1.0
    
  • forme import random as alea

    >>> import random as alea
    >>> alea.random(0, 10)
    8
    >>> l = [1, 2, 3]
    >>> alea.shuffle(l)
    >>> l
    [1, 3, 2]
    

1.2.2. Utiliser un module en script

En Python, un module n’étant qu’un ensemble de déclarations faites dans un fichier, il est possible d’utiliser un module en tant que script et l’exécuter.

Si ce module ne contient que des définitions de constantes et fonctions, son exécution ne produit rien.

En revanche, s’il contient des instructions de calculs et/ou d’impressions, ces calculs et/ou impressions sont effectués.

C’est très bien lorsque le module est utilisé en tant que script principal, mais cela peut être facheux à l’exécution d’un autre script qui l’importe : des calculs inutiles peuvent être effectués et des impressions inopportunes peuvent apparaître.

Une solution à cela consiste à placer dans le module toutes les instructions à exécuter dans une instruction conditionnelle (placée en général à la fin du texte)

if __name__ == '__main__':
   # instructions à exécuter

La condition de cette instruction conditionnelle regarde si la variable __name__ vaut '__main__' ce qui est le cas uniquement si le module est importé en tant que script principal.

Avertissement

il y a deux caractères blancs soulignés (underscore) entourant les mots name et main.

On peut profiter de cette possibilité pour effectuer des doctests, vérifiant la conformité du code aux spécifications décrites dans les docstrings.

if __name__ == '__main__':
   import doctest
   doctest.testmod()

Ainsi en phase de développement du module, il suffit d’exécuter le module en tant que script principal pour effectuer ces tests. Bien entendu, si le module est importé par un autre script, ces tests ne sont pas exécutés.

1.3. Exemple de conception de module : les nombres complexes

Pour illustrer notre propos, nous souhaitons réaliser un (petit) module sur les nombres complexes.

Note

les nombres complexes sont prédéfinis en Python. Pour écrire litéralement le nombre complexe \(a + ib\) en Python, on utilise la lettre j pour désigner le nombre complexe \(i\) (dont le carré vaut -1) en la plaçant derrière la partie imaginaire \(b\) sans espace.

>>> z = 1 + 2j
>>> type(z)
<class 'complex'>
>>> z + z
(2+4j)
>>> z * z
(-3+4j)

Notre module sur les nombres complexes n’est donc présenté que pour des raisons pédagogiques.

Nous allons donner deux versions de réalisation de ce module

  1. une version avec des fonctions
  2. une version objet

1.3.1. Interface

Commençons par définir l’interface de ce module, c’est-à-dire l’ensemble des fonctionnalités qu’il nous offre.

Chacune de ces fonctions sera décrite par une spécification précisant

  • son nom (pleinement qualifié)
  • ses paramètres
  • la valeur qu’elle renvoie ou l’effet qu’elle produit
  • les contraintes d’utilisation (UC pour usage constraint)
  • et éventuellement des exemples d’utilisation.

1.3.1.1. Constructeurs

Commençons par les constructeurs, autrement dit les fonctions permettant de construire des nombres complexes à partir des types de base (int ou float).

Ces constructeurs sont au nombre de deux :

  • Complex.src.complex1.create(real_part, imag_part)[source]

    create a complex number with real part and imaginary part

    Paramètres:
    • real_part (int or float) – the real part of the complex number to create
    • imag_part – the imaginary part of the complex number to create
    Renvoie:

    the complex number real_part + i imag_part

    Type renvoyé:

    complex

    UC:

    none

    Example:
    >>> z = create(1, 2)
    >>> get_real_part(z)
    1.0
    >>> get_imag_part(z)
    2.0
    
  • Complex.src.complex1.from_real_number(x)[source]

    create the complex number x + i0 from real number x

    Paramètres:x (int or float) – a real number
    Renvoie:the complex number x + 0i
    Type renvoyé:complex
    UC:none
    Example:
    >>> z = from_real_number(1)
    >>> get_real_part(z)
    1.0
    >>> get_imag_part(z)
    0.0
    

1.3.1.2. Sélecteurs

Passons aux sélecteurs.

  • Complex.src.complex1.get_real_part(z)[source]

    return the real part of complex number z

    Paramètres:z (complex) – a complex number
    Renvoie:the real part of z
    Type renvoyé:float
    UC:none
    Example:
    >>> z = create(1, 2)
    >>> get_real_part(z)
    1.0
    
  • Complex.src.complex1.get_imag_part(z)[source]

    return the imaginary part of complex number z

    Paramètres:z (complex) – a complex number
    Renvoie:the imaginary part of z
    Type renvoyé:float
    UC:none
    Example:
    >>> z = create(1, 2)
    >>> get_imag_part(z)
    2.0
    

1.3.1.3. Comparaisons

Une fonction de comparaison permettant de tester l’égalité de deux nombres complexes.

  • Complex.src.complex1.are_equals(z1, z2)[source]
    return True if complex numbers z1 and z2 are equals

    False otherwise

    Paramètres:
    • z1 (complex) – a complex number
    • z2 (complex) – a complex number
    Renvoie:

    True if z1 = z2, False otherwise

    Type renvoyé:

    bool

    UC:

    none

    Example:
    >>> z1 = create(1, 2)
    >>> z2 = create(1, 2)
    >>> z3 = create(1, -1)
    >>> are_equals(z1, z2)
    True
    >>> are_equals(z1, z3)
    False
    

1.3.1.4. Fonctions de calcul

Voici maintenant quelques fonctions de calculs sur les nombres complexes.

  • Complex.src.complex1.modulus(z)[source]

    return the modulus of complex number z, ie \(\sqrt{x^2 + y^2}\) if \(z=x+yi\).

    Paramètres:z (complex) – a complex number
    Renvoie:his modulus
    Type renvoyé:float
    UC:none
    Example:
    >>> modulus(create(0, 0))
    0.0
    >>> modulus(create(3, 4))
    5.0
    
  • Complex.src.complex1.add(z1, z2)[source]

    return the sum of the two complex numbers z1 and z2

    Paramètres:
    • z1 (complex) – a complex number
    • z2 (complex) – a complex number
    Renvoie:

    z1 + z2

    Type renvoyé:

    complex

    UC:

    none

    Example:
    >>> z = add(create(1, 2), create(3, 4))
    >>> get_real_part(z)
    4.0
    >>> get_imag_part(z)
    6.0
    
  • Complex.src.complex1.mul(z1, z2)[source]

    return the product of the two complex numbers z1 and z2

    Paramètres:
    • z1 (complex) – a complex number
    • z2 (complex) – a complex number
    Renvoie:

    z1 * z2

    Type renvoyé:

    complex

    UC:

    none

    Example:
    >>> z = mul(create(1, 2), create(3, 4))
    >>> get_real_part(z)
    -5.0
    >>> get_imag_part(z)
    10.0
    

1.3.1.5. Imprimeur

Une fonction d’impression des nombres complexes dans une forme lisible et compréhensible.

  • Complex.src.complex1.print(z, end='\n')[source]

    print the complex number z with algebraic form x + yi where x = real part of z and y = imaginary part

    Paramètres:
    • z (complex) – complex number to print
    • end (string) – [optional] separator (default is “\n”)
    Renvoie:

    None

    UC:

    none

    Example:
    >>> z = create(1, 2)
    >>> print(z)
    1.000000 + 2.000000i
    

Avertissement

le choix du nom print est naturel pour une fonction d’impression. Mais il faut bien prendre garde que si on utilise la forme d’importation de module from complex1 import * alors toute référence à print dans le script qui importe sera une référence à la fonction print du module complex1. En particulier la fonction print du langage de base ne sera plus accessible (sauf si on la qualifie pleinement avec builtins.print).

1.3.2. Une réalisation

Passons maintenant à l’implémentation de ce module.

Une première possibilité pour représenter les nombres complexes est d’utiliser les structures de dictionnaire. Ainsi, nous pouvons envisager de représenter les nombres complexes avec des dictionnaires à deux champs 're' pour la partie réelle et 'im' pour la partie imaginaire. Ainsi le nombre complexe \(1+2i\) sera représenté par le dictionnaire {'re': 1, 'im':2}.

Il est alors facile de réaliser la fonction create :

def create(x,y):
    return {'re' : x, 'im' : y}

Cette fonction répond bien à ce qu’on attend d’elle.

>>> create(1,2)
{'im': 2, 're': 1}

Mais elle n’est pas entièrement satisfaisante parce qu’elle permet de construire des objets qui ne sont manifestement pas des représentations de nombres complexes.

>>> create(True, "Timoleon")
{'re': 'Timoleon', 'im': True}

Il faut donc préalablement à la construction du dictionnaire, que cette fonction vérifie que ses paramètres sont du bon type. En voici donc une bien meilleure réalisation obtenue à l’aide d’assertions établies grâce à l’instruction assert.

def create(real_part, imag_part):
    """
    create a complex number with real part  and imaginary part 

    :param real_part: the real part of the complex number to create
    :type real_part: int or float
    :param imag_part: the imaginary part of the complex number to create
    :type real_part: int or float
    :return: the complex number real_part + i imag_part
    :rtype: complex
    :UC: none
    :Example:

    >>> z = create(1, 2)
    >>> get_real_part(z)
    1.0
    >>> get_imag_part(z)
    2.0
    """
    assert type(real_part) in {int, float}, 'first argument is not int or float' 
    assert type(imag_part) in {int, float}, 'second argument is not int or float' 
    return {'re' : float(real_part), 'im' : float(imag_part)}
>>> create(1, 2)
{'im': 2, 're': 1}
>>> create(True, "Timoleon")
Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    create (True, "Timoleon")
  File "/home/eric/Enseignement/Licence/AP2/Modules/Complex/src/complex1.py", line 38, in create
    assert type(x) in {int,float}, 'first argument is not int or float'
AssertionError: first argument is not int or float

Le second constructeur est alors simple à réaliser.

def from_real_number(x):
    """
    create the complex number x + i0 from real number x

    :param x: a real number
    :type x: int or float
    :return: the complex number x + 0i
    :rtype: complex
    :UC: none
    :Example:

    >>> z = from_real_number(1)
    >>> get_real_part(z)
    1.0
    >>> get_imag_part(z)
    0.0
    """
    return create(x, 0)

Note

Inutile de placer une assertion sur le paramètre x de cette fonction. La fonction create se charge de la vérification.

>>> from_real_number(True)
Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    from_real_number (True)
  File "/home/eric/Enseignement/Licence/AP2/Modules/Complex/src/complex1.py", line 58, in from_real_number
    return create (x,0)
  File "/home/eric/Enseignement/Licence/AP2/Modules/Complex/src/complex1.py", line 38, in create
    assert type(x) in {int,float}, 'first argument is not int or float'
AssertionError: first argument is not int or float

Les sélecteurs get_real_part et get_imag_part s’écrivent tout simplement en utilisant l’accès aux valeurs associées aux clés d’un dictionnaire.

def get_real_part(z):
    """
    return the real part of complex number z

    :param z: a complex number
    :type z: complex
    :return: the real part of z
    :rtype: float
    :UC: none
    :Example:

    >>> z = create(1, 2)
    >>> get_real_part(z)
    1.0
    """
    return z['re']
def get_imag_part(z):
    """
    return the imaginary part of complex number z

    :param z: a complex number
    :type z: complex
    :return: the imaginary part of z
    :rtype: float
    :UC: none
    :Example:

    >>> z = create(1, 2)
    >>> get_imag_part(z)
    2.0
    """
    return z['im']

Pour le test d’égalité, il suffit d’écrire l’expression booléenne qui traduit l’idée que deux nombres complexes sont égaux si et seulement si leurs parties réelles et leurs parties imaginaires le sont.

def are_equals(z1, z2):
    """
    return True if complex numbers z1 and z2 are equals
           False otherwise

    :param z1: a complex number
    :type z1: complex
    :param z2: a complex number
    :type z2: complex
    :return: True if z1 = z2, False otherwise
    :rtype: bool
    :UC: none
    :Example:

    >>> z1 = create(1, 2)
    >>> z2 = create(1, 2)
    >>> z3 = create(1, -1)
    >>> are_equals(z1, z2)
    True
    >>> are_equals(z1, z3)
    False
    """
    return get_real_part(z1) == get_real_part(z2) and get_imag_part(z1) == get_imag_part(z2) 

Le module d’un nombre complexe \(z=x+yi\) est le nombre réel \(\sqrt{x^2+y^2}\). Pour réaliser la fonction modulus nous allons être amené à utiliser la fonction sqrt du module math. Il faut donc importer ce module. Nous plaçons donc l’instruction import math dans notre module, et il est classique de le faire en tête de toutes les déclarations.

Ceci étant fait, nous pouvons écrire la fonction modulus avec la fonction sqrt en qualifiant pleinement son nom, c’est-à-dire en péfixant son nom du nom du module dans lequel elle est définie.

def modulus(z):
    """
    return the modulus of complex number z, ie :math:`\sqrt{x^2 + y^2}` 
    if :math:`z=x+yi`.

    :param z: a complex number
    :type z: complex
    :return: his modulus
    :rtype: float
    :UC: none
    :Example:

    >>> modulus(create(0, 0))
    0.0
    >>> modulus(create(3, 4))
    5.0
    """
    x = get_real_part(z)
    y = get_imag_part(z)
    return math.sqrt(x ** 2 + y ** 2)

Les fonctions d’addition et de multiplication se réalisent sans problème.

def add(z1, z2):
    """
    return the sum of the two complex numbers z1 and z2
    
    :param z1: a complex number
    :type z1: complex
    :param z2: a complex number
    :type z2: complex
    :return: z1 + z2
    :rtype: complex
    :UC: none
    :Example:

    >>> z = add(create(1, 2), create(3, 4))
    >>> get_real_part(z)
    4.0
    >>> get_imag_part(z)
    6.0
    """
    x1 = get_real_part(z1)
    y1 = get_imag_part(z1)
    x2 = get_real_part(z2)
    y2 = get_imag_part(z2)
    return create(x1 + x2, y1 + y2)
def mul(z1, z2):
    """
    return the product of the two complex numbers z1 and z2
    
    :param z1: a complex number
    :type z1: complex
    :param z2: a complex number
    :type z2: complex
    :return: z1 * z2
    :rtype: complex
    :UC: none
    :Example:

    >>> z = mul(create(1, 2), create(3, 4))
    >>> get_real_part(z)
    -5.0
    >>> get_imag_part(z)
    10.0
    
    """
    x1 = get_real_part(z1)
    y1 = get_imag_part(z1)
    x2 = get_real_part(z2)
    y2 = get_imag_part(z2)
    return create(x1 * x2 - y1 * y2, x1 * y2 + y1 * x2)

Reste la fonction print à réaliser.

Pour cela nous allons introduire une fonction auxiliaire qui ne fait pas partie de l’interface du module : ce sera donc une fonction privée. Une convention du langage Python veut que les déclarations privées dans un module aient un identificateur débutant par un tiret bas (_).

Cette fonction est destinée à fournir une représentation sous forme de chaîne de caractères du nombre complexe qu’on lui passe en paramètre. C’et pourquoi nous la nommerons __to_string en respectant la convention Python (avec ici deux tirets bas). En voici une spécification :

  • Complex.src.complex1.__to_string(z)[source]

    return a string representation of the complex number z with algebraic form x+yi where x = real part of z and y = imaginary part

    Paramètres:z (complex) – complex number to convert
    Renvoie:a string representation of z
    Type renvoyé:string
    UC:none
    Example:
    >>> z = create(1, 2)
    >>> __to_string(z)
    '1.000000 + 2.000000i'
    

Les chaînes de caractères produites par cette fonction auront toutes le même schéma : ' xxx + yyy i'xxx est à remplacer par une représentation du nombre flottant qui est la partie réelle du nombre complexe à convertir, et yyy par celle de sa partie imaginaire.

Pour cela il suffit de construire une chaîne de caractères de la forme précédente dans laquelle les xxx et yyy sont remplacés par des marqueurs de nombres flottants : en Python3 c’est {:f}.

>>> schema = '{:f} + {:f} i'
>>> schema
'{:f} + {:f} i'

Puis on utilise la méthode format des chaînes de caractères pour indiquer par quoi chacun des deux marqueurs doivent être remplacés.

>>> schema.format(1,2)
'1.000000 + 2.000000 i'

On en déduit une réalisation possible pour notre fonction __to_string.

def __to_string(z):
    """
    return a string representation of the complex number z with algebraic form
    `x+yi` where x = real part of z and y = imaginary part

    :param z: complex number to convert
    :type z: complex
    :return: a string representation of z
    :rtype: string
    :UC: none
    :Example:

    >>> z = create(1, 2)
    >>> __to_string(z)
    '1.000000 + 2.000000i'
    """
    return '{:f} + {:f}i'.format(get_real_part(z), get_imag_part(z))

Nous en arrivons donc à une première réalisation de la fonction print de notre module.

def print(z, end='\n'):
    print(__to_string(z), end=end)

Malheureusement, cette réalisation ne donne pas le résultat attendu.

>>> z = create(1, 2)
>>> print(z)
Traceback (most recent call last):
  ...
    return 'x = {:f}'.format(x)
TypeError: non-empty format string passed to object.__format__

Que se passe-t-il ? L’erreur de programmation commise réside dans le fait de faire appel à la fonction prédéfinie print pour réaliser la fonction print de notre module. Or justement, par le fait de définir une fonction portant le même nom que la fonction prédéfinie, cette fonction prédéfinie est masquée par la fonction du module. On ne peut donc plus accéder à la fonction prédéfinie par son simple nom.

Alors que faire ?

  • solution 1 : renoncer à utiliser le nom print pour notre fonction, mais c’est dommage
  • solution 2 : utiliser une autre fonction que la fonction prédéfinie print (par exemple sys.stdout.write)
  • solution 3 : savoir que toutes les fonctions prédéfinies le sont dans un module nommé builtins.

C’est cette dernière solution que nous allons adopter.

Pour cela il nous faut

  • ajouter une instruction d’importation du module builtins (comme pour le module math précédemment),
  • puis, pleinement qualifier le nom de la fonction prédéfinie dans le corps de notre fonction print.
def print(z, end='\n'):
    """
    print the complex number z with algebraic form `x + yi`
    where x = real part of z and y = imaginary part

    :param z: complex number to print
    :type z: complex
    :param end: [optional] separator (default is '\\\\n')
    :type end: string
    :return: None
    :UC: none
    :Example:

    >>> z = create(1, 2)
    >>> print(z)
    1.000000 + 2.000000i
    """
    builtins.print(__to_string(z), end=end)

1.3.3. Une autre réalisation

Une autre possibilité pour représenter les nombres complexes est d’utiliser les tuples. Ainsi, nous pouvons envisager de représenter les nombres complexes avec des couples de deux nombres : par exemple, le nombre complexe \(1+2i\) sera représenté par le couple (1,2).

Il est facile de réécrire les constructeurs et sélecteurs en tenant compte de ce choix de représentation, toutes les autres fonctions restant inchangées, grâce à l’usage exclusif des contructeurs et sélecteurs.

1.3.4. Un programme utilisant notre module

Le programme ci-dessous

  • construit deux nombres complexes z1 et z2 à partir de quatre nombres passés sur la ligne de commande
  • les imprime ainsi que leur module
  • et imprime finalement leur somme.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
	A little program using complex module. 
	
"""

import sys
import complex1


def main(x1, x2, x3, x4):


    z1 = complex1.create(x1, x2)
    z2 = complex1.create(x3, x4)

    print()
    print('z1 = ',end='')
    complex1.print(z1)
    print ("z1's modulus = {:f}".format(complex1.modulus (z1)))
    print()

    print('z2 = ',end='')
    complex1.print(z2)
    print("z2's modulus = {:f}".format (complex1.modulus (z2)))
    print()
    
    print('z1 + z2 = ', end='')
    complex1.print(complex1.add(z1, z2))
    print()
    
    print('z1 * z2 = ', end='')
    complex1.print(complex1.mul(z1, z2))
    print()

    

def usage():
    print('Usage : {:s} x1 y1 x2 y2'.format(sys.argv[0]))
    print('with x1, y1, x2, y2 real numbers')
    exit(1)


if __name__ == "__main__":    

    if len(sys.argv) != 5:
        usage()
    else:
        try:
            x1 = float(sys.argv[1])
            y1 = float(sys.argv[2])
            x2 = float(sys.argv[3])
            y2 = float(sys.argv[4])
        except ValueError:
            print('Not a number')
            usage()
        main(x1, y1, x2, y2)

En supposant que ce script est dans un fichier nommé main1.py, voici deux traces de son exécution dans un terminal de commandes (le symbole $ désigne l’invite de commande dans un terminal de commandes, il ne faut pas l’écrire) :

  • $ python3 main1.py
    Usage : main.py x1 x2 x3 x4
    with x1, x2, x3, x4 real numbers
    

    le programme est lancé en invoquant l’interpréteur du langage (commande python3) suivi du nom du fichier contenant ce programme (main.py).

    Mais l’absence d’arguments supplémentaires sur la ligne de commande provoque l’appel à la fonction usage (cf lignes 20-21 du code source), qui imprime l’usage correct de notre programme.

  • $ python3 main1.py 1 2 3 4
    z1 = 1.000000 + 2.000000i
    z1's module = 2.236068
    
    z2 = 3.000000 + 4.000000i
    z2's module = 5.000000
    
    z1 + z2 = 4.000000 + 6.000000i
    

    Cette fois quatre nombres ont été fournis dans la ligne de commande, et notre programme fonctionne parfaitement.

1.4. Créer un nouveau type : les classes

1.4.1. Motivation

Le module complex1 réalisé ci-dessus, s’il répond bien aux spécifications attendues, n’est pas satisfaisant du point de vue du développement logiciel pour plusieurs raisons.

Lorsqu’on construit un complexe avec la fonction create

>>> z1 = complex1.create(1, 2)
>>> z1
{'im' = 2.0, 're' = 1.0}

l’utilisateur du module voit que les nombres complexes sont des dictionnaires, et il peut contourner les constructeurs et sélecteurs offerts par le langage

>>> z2 = {'re' = 3.0, 'im' = 4.0}
>>> z2['re']
3.0
>>> complex1.add(z1, z2)
{'im' = 6.0, 're' = 4.0}

Les deux premiers inconvénients du module complex1 sont donc

  1. la non abstraction des nombres complexes
  2. qui permet le contournement de l’interface du module.

Cela peut sembler être des inconvénients mineurs mais ce n’est pas le cas car cela peut avoir des conséquences importantes en développement logiciel.

En effet, supposez que vous ayez écrit un gros projet en utilisant le module complex1 dont vous n’êtes pas l’auteur. Peu scrupuleux vous avez contourné l’interface pour construire des nombres complexes, mais peu importe votre projet fonctionne parfaitement. Plus tard suite à une mise à jour du module complex1 votre projet ne fonctionne plus ! Vous consultez la documentation de l’interface du module et vous ne constatez aucune différence avec la version précédente. Étrange ! Que s’est-il passé ?

La réponse réside dans le fait que l’auteur de la nouvelle version du module a changé son implémentation (par exemple pour améliorer l’efficacité des fonctionnalités) tout en gardant l’interface (pour ne pas nuire aux utilisateurs). Et dans cette nouvelle version, les nombres complexes ne sont plus représentés par des dictionnaires à deux champs 're' et 'im'. Vos contournements de constructeurs et sélecteurs définis par l’interface sont la source de vos problèmes.

Un autre défaut du module complex1 concerne le typage.

>>> type(z1)
<class 'dict'>

Le type de la valeur de z1 est un dictionnaire et pas un nombre complexe. Là encore cela peut paraître anodin.

Mais si dans un programme, vous avez besoin de contrôler le type de certaines données, et que dans ce programme vous manipulez des nombres complexes et d’autres valeurs elles aussi représentées par des dictionnaires, comment pourrez-vous les distinguer ?

>>> etudiant = {'nom' = 'Calbuth', 'prenom' = 'Raymond'}
>>> type(z1) == type(etudiant)
True

Le troisième inconvénient du module complex1 est donc

  1. la non création d’un nouveau type distincts des autres types de données existant.

Il nous faut donc trouver un moyen de définir

  • de nouveaux types de données
  • tout en gardant abstraite leur implantation
  • et en rendant impossible le contournement de l’interface.

Certains langages offrent au programmeur des moyens de déclaration de nouveaux types. En Python, un de ces moyens est de passer par la programmation orientée objet en définissant de nouvelles classes.

1.4.2. Exemples d’objets

En Python tout est objet.

>>> type(12)
<class 'int'>
>>> type(12.0)
<class 'float'>
>>> type(True)
<class 'bool'>
>>> type('a')
<class 'str'>
>>> type([1, 2, 3])
<class 'list'>
>>> type((1, 2, 3))
<class 'tuple'>
>>> type({1, 2, 3})
<class 'set'>
>>> type({'a' = 1})
<class 'dict'>

Les types de données en Python sont tous définis par des classes. Les classes permettent de définir des objets qui ont des attributs et des méthodes.

Vous connaissez déjà certaines méthodes pour certaines des classes citées ci-dessus :

>>> s = 'Timoleon'
>>> s.upper()
'TIMOLEON'
>>> s.index('i')
1
>>> s.split('o')
['Tim', 'le', 'n']
>>> ';'.join([['Tim', 'le', 'n']])
'Tim;le;n'

Les méthodes peuvent être apparentées à des fonctions liées aux objets. On verra que leur définition ressemble beaucoup à celle des fonctions.

1.4.3. Conception d’une classe

Voyons comment définir une nouvelle classe que nous nommerons A (contrairement aux classes prédéfinies de Python désignées par des identificateurs en lettres minuscules, les classes que nous définirons seront désignées par un identificateur débutant par une lettre majuscule).

L’en-tête d’une déclaration de classe débute par le mot clé class. La plus petite déclaration de classe syntaxiquement valide est :

class A():
    pass

Bien entendu cette classe n’est pas très intéressante, mais elle nous permet déjà de créer des objets de type A, et d’en étudier le type

>>> x = A()
>>> type(x)
<class '__main__.A'>

La variable x est bien du type A. Nous avons créé un nouveau type distinct de tous les autres.

Note

dans la formulation de la réponse de la fonction type ci-dessus, le préfixe '__main__' suppose que la classe A a été déclarée dans l’environnement principal. Si la classe avait été déclarée dans un module a (dans un fichier a.py), la réponse aurait été

<class 'a.A'>

Quelle est la valeur de la variable x ?

>>> x
<__main__.A object at 0x0x7f7f6bce4898>

La réponse est un peu complexe : la valeur de x est un objet de classe (ou type) A (et cet objet est stocké en mémoire à l’adresse indiquée en hexadécimal dans la réponse). En tout cas rien ne transparaît du contenu de cet objet.

Voyons maintenant comment enrichir notre classe avec des attributs et des méthodes.

1.4.3.1. Des attributs

Les attributs d’un objet peuvent être vus comme des variables locales à l’objet. Une classe peut définir autant d’attributs qu’on veut ayant des valeurs de n’importe quel type.

class A():
    val = 0

Voyons ce qu’il est possible de faire avec les objets d’une telle classe :

>>> x = A()
>>> y = A()
>>> x.val
0
>>> y.val
0
>>> x.val = 2
>>> x.val
2
>>> y.val
0

C’est la notation pointée qui permet d’accéder à un attribut d’un objet. Comme on le voit ci-dessus, on peut modifier la valeur d’un attribut et deux objets différents d’une même classe ne partagent pas leurs attributs.

1.4.3.2. Des méthodes

Ajoutons une méthode qui permet d’incrémenter la valeur de l’attribut val d’un objet.

class A():
    val = 0

    def incr(self):
        self.val += 1

La méthode incr se déclare de façon analogue à la déclaration d’une fonction. Le nom self du paramètre de cette méthode est une convention de Python pour désigner l’objet sur lequel s’applique la méthode.

Voyons comment invoquer cette méthode sur un objet de cette classe :

>>> x = A()
>>> x.val
0
>>> A.incr(x)
>>> x.val
1

On observe bien que l’attribut val de l’objet x a été incrémenté suite à l’appel à la méthode incr définie par la classe A, appliquée à cet objet x.

En programmation objet on préfère invoquer la méthode d’un objet en préfixant l’appel par l’objet. Et l’objet qui était ci-dessus un argument de la méthode, devient préfixe de l’appel, la méthode n’ayant plus d’argument :

>>> x.incr()
>>> x.val
2

1.4.3.3. Public/privé

Les attributs et méthodes peuvent être publics ou privés.

En Python la distinction entre attributs/méthodes publics et privés se fait par le choix des identificateurs : tout attribut/méthode désigné par un identificateur débutant par deux caractères blancs soulignés (underscore) est considéré comme privé.

Voici notre classe A légèrement modifiée : l’attribut val̀ est renommé __val :

class A():
     __val = 0

     def incr(self):
         self.__val += 1
>>> x = A()
>>> x.incr()
>>> x.__val
...
AttributeError: 'A' object has no attribute '__val'

Comme on peut le constater, bien qu’existant, l’attribut __val est inaccessible à l’utilisateur d’un objet de la classe A : c’est un attribut privé.

Si on veut pouvoir accéder à la valeur d’un attribut privé, il faut définir un sélecteur (ou accesseur) correspondant. C’est le rôle de la méthode get_val définie dans cette nouvelle version de notre classe :

class A():
     __val = 0

     def incr(self):
         self.__val += 1

     def get_val(self):
         return self.__val
>>> x = A()
>>> x.get_val()
0
>>> x.incr()
>>> x.get_val()
1

1.4.3.4. Construction paramétrée d’un objet

Lorsqu’ils sont créés, nos objets de la classe A ont tous un attribut privé initialisé à 0. Nous allons voir comment créer des objets avec des valeurs passées en paramètre à la construction.

Pour cela, il faut ajouter à notre classe une méthode spéciale nommée __init__ (deux blancs soulignés avant et après init). Cette méthode spéciale, lorsqu’elle est présente, est automatiquement appelée à la création d’un objet.

Dans notre classe cette méthode __init__ prend un autre paramètre que self : c’est la valeur initial qu’on désire donner à l’attribut privé __val. À noter que dans la déclaration de notre classe, il n’y a plus de ligne pour l’attribut __val : celui-ci sera automatiquement créé et initialisé par la méthode __init__ à la création des objets.

class A():

     def __init__(self, init_val):
         assert type(init_val) == int, 'init_val must be an int'
         self.__val = init_val

     def incr(self):
         self.__val += 1

     def get_val(self):
         return self.__val

La construction d’un objet se fait alors en donnant les arguments autre que ̀̀self`` à la classe A :

>>> x = A(12)
>>> x.get_val()
12

Sans paramètre la construction est impossible :

>>> A()
...
TypeError: __init__() missing 1 required positional argument: 'init_val'

et avec un paramètre non entier aussi :

>>> A('a')
...
AssertionError: init_val must be an int

Si on veut garder la possibilité de construire un objet sans passer de paramètre étant entendu que la valeur par défaut sera 0, il suffit de donner cette valeur par défaut au paramètre :

class A():

     def __init__(self, init_val=0):
         assert type(init_val) == int, 'init_val must be an int'
         self.__val = init_val

     def incr(self, inc=1):
         self.__val += 1

     def get_val(self):
         return self.__val
>>> x = A(12)
>>> x.get_val()
12
>>> y = A()
>>> y.get_val()
0

1.4.3.5. Méthode non paramétrée par self

Toutes les méthodes ne s’appliquent pas nécessairement à l’objet propriétaire de la méthode. Lorsque c’est le cas, elles ne sont pas paramétrées par self.

Par exemple, voici notre classe avec une méthode random sans paramètre dont la vocation est de construire un objet de la classe A initialisé avec une valeur entière aléatoirement choisie entre -100 (inclus) et 100 (exclu).

class A():

     def __init__(self, init_val):
         assert type(init_val) == int, 'init_val must be an int'
         self.__val = init_val

     def incr(self, inc=1):
         self.__val += inc

     def get_val(self):
         return self.__val

     def random():
         return A(random.randint(-100, 100))

L’appel à cette méthode n’ayant pas du paramètre self ne peut se faire qu’en invoquant la classe :

>>> x = A.random()
>>> x.get_val()
-77

Si on tente d’appeler cette méthode en partant d’un objet, une exception est déclenchée :

>>> x.random()
...
TypeError: random() takes 0 positional arguments but 1 was given

1.4.4. Une classe Complex (première version)

Nous allons donner une deuxième réalisation du module complexe que nous nommerons complex2 (stocké dans un fichier nommé complex2.py). Dans ce module les nombres complexes seront des objets d’une classe que nous nommerons Complex2. Dans cette classe les nombres complexes seront des objets ayant deux attributs privés Complex2.__real_part et Complex2.__imag_part dont les valeurs sont des nombres flottants correspondant aux parties réelle et imaginaire du nombre complexe.

1.4.4.1. Constructeurs

Complex2.__init__(real_part, imag_part)[source]

create a complex number with real part real_part and imaginary part imag_part.

This method is implicitely called at object creation.

Paramètres:
  • real_part (int or float) –
  • imag_part (int or float) –
Raises:

AssertionError if params are not int or float numbers

Example:
>>> z = Complex2(3, 2)
>>> z.get_real_part()
3.0
>>> z.get_imag_part()
2.0
Complex2.from_real_number()[source]

create a complex number with real part x and zero imaginary part.

Paramètres:x – (int or float)
Renvoie:a new complex number x + 0.0i
Type renvoyé:Complex2
UC:none
Example:
>>> z = Complex2.from_real_number(3)
>>> z.get_real_part()
3.0
>>> z.get_imag_part()
0.0

1.4.4.2. Sélecteurs

Complex2.get_real_part()[source]

return the real part of complex number self.

Renvoie:the real part of self
Type renvoyé:float
UC:None
Example:
>>> z = Complex2(3, 2)
>>> z.get_real_part()
3.0
Complex2.get_imag_part()[source]

return the imaginary part of complex number self

Renvoie:the imanigary part of self
Type renvoyé:float
UC:None
Example:
>>> z = Complex2(3, 2)
>>> z.get_imag_part()
2.0

1.4.4.3. Méthodes liées au calcul

Complex2.modulus()[source]
Renvoie:modulus of complex number self, ie \(\sqrt{x^2 + y^2}\) if \(z = x + yi\).
Type renvoyé:float
UC:none
Example:
>>> z = Complex2(3, 2)
>>> z.modulus() == math.sqrt(13)
True
Complex2.add(z)[source]
Paramètres:z (Complex2) –
Renvoie:the sum of complex numbers self and z.
Type renvoyé:Complex2
UC:none
Example:
>>> z = Complex2(1, 2).add(Complex2(3, 4))
>>> z.get_real_part()
4.0
>>> z.get_imag_part()
6.0
Complex2.mul(z)[source]
Paramètres:z (Complex2) –
Renvoie:the product of complex numbers self and z.
Type renvoyé:Complex2
UC:none
Example:
>>> z = Complex2(1, 2).mul(Complex2(3, 4))
>>> z.get_real_part()
-5.0
>>> z.get_imag_part()
10.0

1.4.4.4. Méthode de comparaison

Complex2.equals(z)[source]
Renvoie:
  • True if complex number self equals complex number z2
  • False otherwise
Type renvoyé:bool
UC:none
Example:
>>> z = Complex2(1, 2)
>>> z.equals(Complex2(1, 2))
True
>>> z.equals(Complex2(-1, 2))
False

1.4.4.5. Utilisation de la classe Complex2

Le programme ci-dessous

  • importe le module complex2
  • construit deux nombres complexes z1 et z2 à partir de quatre nombres passés sur la ligne de commande
  • les imprime ainsi que leur module
  • et imprime finalement leur somme.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
	A little program using complex module. 
	
"""

import sys
import complex2


def main(x1, x2, x3, x4):

    z1 = complex2.Complex2(x1, x2)
    z2 = complex2.Complex2(x3, x4)

    print()
    print('z1 = {:f} + {:f}i'.format(z1.get_real_part(), z1.get_imag_part()))
    print ("z1's modulus = {:f}".format(z1.modulus ()))
    print()
    
    print('z2 = {:f} + {:f}i'.format(z2.get_real_part(), z2.get_imag_part()))
    print ("z2's modulus = {:f}".format(z2.modulus ()))
    print()
    
    z3 = z1.add(z2)
    print('z1 + z2 = {:f} + {:f}i'.format(z3.get_real_part(), z3.get_imag_part()))
    print()
    
    z4 = z1.mul(z2)
    print('z1 * z2 = {:f} + {:f}i'.format(z4.get_real_part(), z4.get_imag_part()))
    print()
    
def usage():
    print('Usage : {:s} x1 y1 x2 y2'.format(sys.argv[0]))
    print('with x1, y1, x2, y2 real numbers')
    exit(1)

if __name__ == "__main__":    

    if len(sys.argv) != 5:
        usage()
    else:
        try:
            x1 = float(sys.argv[1])
            y1 = float(sys.argv[2])
            x2 = float(sys.argv[3])
            y2 = float(sys.argv[4])
        except ValueError:
            print('Not a number')
            usage()
        main(x1, y1, x2, y2)

En supposant que ce script est dans un fichier nommé main2.py, voici une trace de son éxécution dans un terminal de commandes :

$ python3 main2.py 1 2 3 4

z1 = 1.000000 + 2.000000i
z1's module = 2.236068

z2 = 3.000000 + 4.000000i
z2's module = 5.000000

z1 + z2 = 4.000000 + 6.000000i

1.4.5. Méthodes spéciales en Python

Dans la définition d’une classe en Python, il est possible de déclarer certaines méthodes dites spéciales (ou encore magiques) qui offrent un certain nombre de services et facilités dans la manipulation des objets de la classe.

Voici une liste (incomplète) de ces méthodes dont les noms sont imposés et sont tous entourés de deux blancs soulignés.

Quelques méthodes spéciales
nom rôle
__repr__ représentation externe de l’objet
__str__ transformation de l’objet en chaîne de caractères
__add__ pour utiliser l’opérateur +
__sub__ pour utiliser l’opérateur (binaire) -
__mul__ pour utiliser l’opérateur *
__div__ pour utiliser l’opérateur /
__neg__ pour utiliser l’opérateur (unaire) -
__eq__ pour utiliser l’opérateur ==
__neq__ pour utiliser l’opérateur !=
__lt__ pour utiliser l’opérateur <
__le__ pour utiliser l’opérateur <=
__gt__ pour utiliser l’opérateur >
__ge__ pour utiliser l’opérateur >=

1.4.6. Une classe Complex (seconde version)

Cette seconde version est nommée Complex (et elle se trouve dans un module stocké dans un fichier nommé complex3.py).

Cette classe définit exactement la même représentation des nombres complexes que la classe précédente et contient les mêmes méthodes. Elle est enrichie de plusieurs méthodes spéciales.

1.4.6.1. Méthodes spéciales liées à la représentation externe

Complex.__repr__()[source]
Renvoie:an external representation of complex number self (for visualizing complex numbers value in interactive mode for example)
Type renvoyé:str
UC:none
Example:
>>> Complex(3, 2)
Complex(3.0, 2.0)
Complex.__str__()[source]
Renvoie:an external representation of complex number self (for printing complex numbers for example)
Type renvoyé:str
UC:none
Example:
>>> z = Complex(3, 2)
>>> z.__str__()
'3.000000 + 2.000000i'
>>> print(z)
3.000000 + 2.000000i

1.4.6.2. Méthodes spéciales liées à l’arithmétique

Complex.__abs__()[source]
Renvoie:modulus of self
Type renvoyé:float
UC:none
Example:
>>> z = Complex(3, 2)
>>> abs(z) == math.sqrt(13)
True
Complex.__add__(z)[source]
Renvoie:the sum of complex numbers self and z
Type renvoyé:Complex
UC:none
Example:
>>> Complex(1, 2) + Complex(3, 4)
Complex(4.0, 6.0)
Complex.__mul__(z)[source]
Renvoie:the product of complex numbers self and z
Type renvoyé:Complex
UC:none
Example:
>>> Complex(1, 2) * Complex(3, 4)
Complex(-5.0, 10.0)
Complex.__neg__()[source]
Renvoie:the opposite complex number -self
Type renvoyé:Complex
UC:none
Example:
>>> -Complex(1, 2)
Complex(-1.0, -2.0)
Complex.__sub__(z)[source]
Renvoie:the complex number self - z
Type renvoyé:Complex
UC:none
Example:
>>> Complex(1, 2) - Complex(3, 4)
Complex(-2.0, -2.0)

1.4.6.3. Méthodes spéciales pour la comparaison

Complex.__eq__(z)[source]
Renvoie:
  • True if complex numbers self and z are equals
  • False otherwise
Type renvoyé:bool
UC:none
Example:
>>> Complex(3, 2) == Complex(3, 2)
True
>>> Complex(3, 2) == Complex(2, 2)
False
Complex.__neq__(z)[source]
Renvoie:
  • True if complex numbers self and z are not equals
  • False otherwise
Type renvoyé:bool
UC:none
Example:
>>> Complex(3, 2) != Complex(2, 2)
True
>>> Complex(3, 2) != Complex(3, 2)
False

1.4.6.4. Utilisation de la classe Complex2

Le programme ci-dessous

  • importe le module complex3
  • construit deux nombres complexes z1 et z2 à partir de quatre nombres passés sur la ligne de commande
  • les imprime ainsi que leur module
  • et imprime finalement leur somme.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
	A little program using complex module. 
	
"""

import sys
import complex3


def main(x1, x2, x3, x4):
    
    z1 = complex3.Complex(x1, x2)
    z2 = complex3.Complex(x3, x4)

    print()
    print('z1 =', z1)
    print ("z1's modulus = {:f}".format(abs(z1)))
    print()
    
    print('z2 =', z2)
    print ("z2's modulus = {:f}".format(abs(z2)))
    print()

    z3 = z1 + z2
    print('z1 + z2 =', z3)
    print()

    z4 = z1 * z2
    print('z1 * z2 =', z4)
    print()


def usage():
    print('Usage : {:s} x1 y1 x2 y2'.format(sys.argv[0]))
    print('with x1, y1, x2, y2 real numbers')
    exit(1)

if __name__ == "__main__":    

    if len(sys.argv) != 5:
        usage()
    else:
        try:
            x1 = float(sys.argv[1])
            y1 = float(sys.argv[2])
            x2 = float(sys.argv[3])
            y2 = float(sys.argv[4])
        except ValueError:
            print('Not a number')
            usage()
        main(x1, y1, x2, y2)

En supposant que ce script est dans un fichier nommé main3.py, voici une trace de son éxécution dans un terminal de commandes :

$ python3 main3.py 1 2 3 4

z1 = 1.000000 + 2.000000i
z1's module = 2.236068

z2 = 3.000000 + 4.000000i
z2's module = 5.000000

z1 + z2 = 4.000000 + 6.000000i