Python pour le calcul scientifique/Éléments de programmation

Un livre de Wikilivres.
Sauter à la navigation Sauter à la recherche

Types de variables[modifier | modifier le wikicode]

Python définit « tout seul » le type de la variable : « 3 » sera un entier (integer), « 3.0 » sera un réel à virgule flottante (float), « "3" » sera une chaîne de caractères (string).

On peut connaître le type d'une variable avec la fonction type().

On peut tester certaines valeurs :

  • np.isnan(x) indique si les valeurs de x sont des NaN (not a number) ; si x est une matrice, le résultat est une matrice de booléens, l'élément [i, j] est True si x[i, j] est un NaN ;
  • np.isinf(x) indique si les valeurs de x sont ±∞ ; si x est une matrice, le résultat est une matrice booléenne de même dimension.

On peut forcer un type :

  • int(x) : transforme la valeur x en nombre entier ;
  • long(x) : " en entier long (précision illimitée) ;
  • float(x) : " en nombre réel à virgule flottante ;
  • str(x) : " en chaîne de caractères ;
  • complex(Re, Im) : crée le nombre complexe Re + Im·j, j désignant la racine carrée de –1 ;
  • list() : crée une liste ;
  • tuple() : crée un n-uplet.

Par exemple

type(3) # <class 'int'>
type(float(3)) # <class 'float'>
complex(1, 1) == 1 + 1j # True
list("blabla") # ['b', 'l', 'a', 'b', 'l', 'a']

Python distingue plusieurs genres de types :

  • Un itérable est un objet dont on peut extraire les éléments un par un ; ce sont les objets pour lesquels on peut écrire for i in iterable:. Il s'agit essentiellement des listes, n-uplets, chaînes de caractères, ensembles, dictionnaires et fichiers.
  • Un modifiable (mutable) est un objet que l'on peut modifier ; par exemple une liste est modifiable — on peut changer la valeur d'un élément, en ajouter ou en enlever un — mais les n-uplets non, pas plus qu'une chaîne de caractères ou un nombre.
  • Un identifiable (hashable, le hashage étant une signature caractéristique d'un objet) : objet possédant un identifiant unique. Un objet identifiable est toujours non-modifiable (unmutable).

Types numériques[modifier | modifier le wikicode]

Entiers[modifier | modifier le wikicode]

Nous pouvons définir les entiers au format octal ou hexadécimal : il faut débuter le nombre par respectivement 0o (le chiffre zéro et la lettre o) et 0x (le chiffre zéro et la lettre x). À l'inverse, la fonction hex() renvoie une chaîne correspondant à l'écriture d'un entier au format hexadécimal, et oct() renvoie la chaîne correspondant à l'éciture en octal. Par exemple :

print(0o10, ";", 0x10)
# 8 ; 16
print(hex(20))
# 0x14

Réels[modifier | modifier le wikicode]

Les réels disposent de fonctions spécifiques appelées « méthodes ».

Une méthode est un fonction spécifique à un type d'objets. Étant conçue ad hoc, elle est souvent plus économe en ressource et en temps qu'une fonction générique. Pour appliquer la méthode meth() à la variable x, on écrit : x.meth().

Nous avons déjà présenté la méthode float.as_integer_ration() qui donne la fraction réduite égale à la valeur du nombre. Les réels disposent de plusieurs autres méthodes :

  • float.trunc() : tronque le nombre réel ;
  • float.floor(), float.ceil() : renvoie l'entier le plus proche, respectivement inférieur ou supérieur ;
  • float.hex() : renvoie une chaîne de caractères correspondant à l'écriture du nombre en hexadécimal.

Par exemple :

a = 20.
print(a.hex())
# 0x1.4000000000000p+4

print(10..hex())
# 0x1.4000000000000p+3

Dans le deuxième exemple, nous appliquons la méthode float.hex() directement au nombre 10. ; le point est obligatoire car sinon, c'est un entier, pour lequel la méthode n'est pas définie.

Notez que la méthode float.hex() est différentes de la fonction hex() : la première concerne les réels, la seconde les entiers.

Complexes[modifier | modifier le wikicode]

Nous avons déjà mentionné la méthode complex.conjugate() qui donne le conjugué du nombre.

Un nombre complexe dispose de deux attributs :

  • complex.real : sa partie réelle ;
  • complex.imag : sa partie imaginaire.

Par exemple :

a = 5+2j
print(a.conjugate(), ";", a.real, ";", a.imag)
# (5-2j) ; 5.0 ; 2.0

Chaînes de caractères[modifier | modifier le wikicode]

Ressources
7. Input and Output sur Python Documentation. Consulté le 2019-04-06

Généralités[modifier | modifier le wikicode]

Il existe en fait trois manières de définir une chaîne de caractères :

  • avec des guillemets simples ou doubles comme vu précédemment : "…" ou bien '…' ;
  • avec trois guillemets doubles : """…""" : cela permet d'avoir une chaîne de caractères s'étendant sur plusieurs lignes, les retours de ligne étant pris en compte ; c'est utilisé en particulier pour la description des fonctions (docstrings, voir ci-après) ;
  • avec des guillemets précédés d'un « r », r"…" ou r'…' : cela permet d'interpréter les barres de fraction inverses « \ » comme un caractère « normal » et non comme un caractère d'échappement (voir ci-après) ; cela est utile lorsque l'on utilise les possibilités LaTeX dans le tracé de graphiques (voir plus loin) ;
  • avec des guillemets précédés d'un « f », f"…" ou f'…' : cela permet d'utiliser des variables formatées (voir ci-après).

Une chaîne de caractères n'est pas modifiable. Si l'on veut remplacer un caractère, l'insérer ou le supprimer, il faut transformer la chaîne en liste, avec la commande list(), puis rassembler la liste en la joignant (join) à une chaîne vide :

chaine = "blabla"
chaineList = list(chaine)
chaineList[2] = "c"
chaine = "".join(chaineList)
print(chaine) # blcbla

Dans une chaîne simple "…" ou '…', on peut introduire un retour à la ligne avec \n.

Substitution de variables[modifier | modifier le wikicode]

Lorsque l'on veut utiliser des variables, on fait précéder les guillemets d'un « f » et l'on écrit les noms de vrariables entre accolades. Par exemple :

monde = "world"

chaine = f"Hello {monde}!"

print(chaine) # Hello world!

On peut indiquer la taille de la chaîne générée à partir de la variable sous la forme {nomVariable:taille}, la taille étant un entier. Par exemple :

chiffre1 = 1
nom1 = "un"
chiffre2 = 2
nom2 = "deux"

chaine = f"{nom1:5} : {chiffre1:5d}\n{nom2:5} : {chiffre2:5d}"

print(chaine)
# un    :     1
# deux  :     2

Vous remarquez que l'on ajoute un « d » pour les entiers décimaux, et que les nombres sont alignés à droite. Si le nombre est un nombre réal à virgule flottante (float), on peut indiquer le nombre de décimales sous la forme .nf :

chaine = f"{np.pi:.5f}"
print(chaine)
# 3.15169

Avec la syntaxe m.nf, on indique également que la totalité du nombre doit occuper m caractères.

Pour convertir un nombre en caractère Unicode correspondant, on utilise la lettre c :

nompi = 0x03c0
chaine = f"{nompi:c} = {np.pi:.5f}"
print(chaine)
# π = 3.14159

La classe str dispose également de la méthode .format(). On indique un n-uplet de chaînes (ou de nombres) à la méthode et l'on met des accolades dans la chaîne principale ; les accolades sont remplacées dans l'ordre des chaînes de la méthode. On peut changer l'ordre en indiquant quelle valeur utiliser dans quelle accolade. Par exemple :

chaine1 = "On compte {} puis {}".format(1, 2)
chaine2 = "On compte {0} puis {1}. Mais à rebours, on compte {1} puis {0}.".format("un", "deux")

print(chaine1, "\n", chaine2)
# On compte 1 puis 2
#  On compte un puis deux. Mais à rebours, on compte deux puis un.

L'utilisation du caractère pourcent « % » permet d'utiliser la mise en forme sprintf() du langage C :

chaine = "π = %.5f" % np.pi
print(chaine)
# π = 3.14159

Méthodes des chaînes[modifier | modifier le wikicode]

Le type str dispose d'un certain nombre de méthodes. Nous avons déjà vu les méthodes str.join() et str.format(), en voici quelques autres :

  • str.capitalize() : met le premier caractère en capitale (majuscule) et les autres en minuscule ;
  • str.lower() : met tout en minuscules (lowercase) ;
  • str.upper() : met tout en capitales (lowercase) ;
  • str.center(n) : met la chaîne au centre d'une chaîne de longueur n, en complétant avec des espaces ; on peut compléter avec d'autres caractères avec str.center(n, c), par exemple "a".center(7, ".") donne "....a...." ;
  • str.ljust(n, c) et str.rjust(n, c) : comme .center() mais la chaîne est respectivement alignée au fer à gauche (left) et à droite (right) ;
  • str.isdigit() : booléen vrai si tous les caractères sont des nombres ;
  • str.find(sous-chaine), str.rfind(sous-chaine) : indique respectivement le premier emplacement et le dernier emplacement de la sous-chaîne dans la chaîne ;
  • str.partition(séparateur) : retourne un triplet avec la portion de chaîne avant le séparateur, le séparateur puis la portion de chaîne après le séparateur ;
  • str.replace(ancien, nouveau) : remplace la chaîne ancien par la chaîne nouveau dans la chaîne ;
  • str.split(séparateur) : découpe la chaîne au niveau des séparateurs et renvoie une liste.

Autres fonctions[modifier | modifier le wikicode]

La fonction chr() transforme un code Unicode en caractère. Par exemple, chr(97) donne "a" et chr(0x03c0) donne "π".

Manipulation de listes[modifier | modifier le wikicode]

Les listes sont une structure de données fondamentale en Python.

Ressources

Copie d'une liste[modifier | modifier le wikicode]

Contrairement à d'autres types, lorsque vos écrivez b = a avec des listes, vous ne créez pas une copie de la variable a, vous créez un alias : l'objet b est un autre nom de l'objet a. En particulier, si vous modifiez b, vous modifiez en fait a. Par exemple :

a = [1, 2, 3, 4]
b = a
b[2] = 5
print(a, b) # [1, 2, 5, 4] [1, 2, 5, 4]

Si l'on veut créer une copie de a, il faut utiliser a[:] ou bien a.copy() :

a = [1, 2, 3, 4]
b = a[:]
c = a.copy()
b[2] = 5
c[2] = 6
print(a, b, c) # [1, 2, 3, 4] [1, 2, 5, 4] [1, 2, 6, 4]

Méthodes de listes[modifier | modifier le wikicode]

Pour modifier une liste, vous disposez des méthodes suivantes :

  • a.append(x) : ajoute l'élément x à la fin de la liste a ;
  • a.extend(x) : ajoute la liste x à la fin de la liste a ;
  • a.append(i, x) : aoute l'élément x avant l'interstice i de la liste a ;
  • x = a.pop(i) : enlève l'élément i de la liste a et le met dans la variable x ; x = a.pop() enlève le dernier élément de la liste ;
  • a.clear() : vide la liste a ;
  • a.sort() : trie la liste par ordre croissant ;
  • a.sort(reverse = True) : trie par ordre décroissant ;
  • a.reverse() : inverse l'ordre de a.

Pour supprimer l'élément à l'indice i, au lieu d'utiliser a.pop(i), on peut aussi utiliser

del(a[i])

Pour trier une liste, on peut aussi utiliser la fonction sorted(), ce qui permet par exemple de conserver la liste originale, non triée : b = sorted(a). La fonction sorted() fonctionne avec tous les objets « itérables » comme par exemple une chaîne de caractères :

a = "ahjbfk"
print(sorted(a)) # ['a', 'b', 'f', 'h', 'j', 'k']

Pour mettre en évidence la performance de la méthode list.sort() par rapport à la fonction générique sorted() :

import numpy as np
import time

a = np.random.rand(int(1e7))
t1 = time.perf_counter()
b = sorted(a) # Fonction générique
t2 = time.perf_counter()
a.sort() # Méthode spécifique
t3 = time.perf_counter()
print("Sorted :", t2-t1, " s ; .sort :", t3-t2, "s ; rapport :", (t2-t1)/(t3-t2))
# Sorted : 14.2...  s ; .sort : 1.1... s ; rapport : 12.6...

Par rapport à une valeur donnée :

  • a.remove(x) : retire la première occurrence de la valeur x de la liste a ;
  • a.index(x) : indique l'indice où se trouve la première occurrence de la valeur x ;
  • a.count(x) : indique le nombre de fois que l'on trouve la valeur x dans la liste a.

Définition en compréhension[modifier | modifier le wikicode]

La définition en compréhension (list comprehension) est une méthode permettant de construire des listes en indiquant simplement des axiomes, des consignes de filtrage. Cette méthode est élégante car proche de la notation mathématique et compacte, mais c'est une méthode itérative donc lente par rapport à une méthode vectorisée fournie par le module NumPy.

Par exemple, pour créer la liste des carrés des nombres entiers entre 0 et 9, il suffit d'écrire

carre = [x**2 for x in range(10)]

ce qui se rapproche de la notation d'ensemble .

Si l'on veut la liste des nombres strictement inférieurs à 20 dont le carré est supérieur à 10, on peut écrire :

X = [x for x in range(20) if x**2 > 10]

ce qui se rapproche de la notation d'ensemble .

Pour mettre en évidence la performance du calcul vectorisé par rapport à la méthode itérative :

import time
import numpy as np

n = int(1e7) # taille de la liste
t1 = time.perf_counter()
carre = [x**2 for x in range(n)] # Définition en compréhension
t2 = time.perf_counter()
carre2 = np.arange(n)**2 # Calcul vectorisé
t3 = time.perf_counter()
print("En compréhension : ", t2-t1, "s ; vectorisé :", t3-t2, "s ; rapport :", (t2-t1)/(t3-t2))
# En compréhension :  4.515... s ; vectorisé : 0.156... s ; rapport : 28.982...

Structure d'un programme[modifier | modifier le wikicode]

Un programme est simplement une suite d'instructions.

Dans les environnements Unix BSD, un programme Python peut être considéré comme un script c'est-à-dire qu'il suffit de taper son nom dans l'invite de commande (shell) sans avoir à invoquer python. Le programme doit alors commencer par un en-tête normalisé surnommé shebang :

#!/usr/bin/env python3

Ce shebang est inutile avec Jupyter.

L'en-tête peut également contenir la description de l'encodage du fichier texte, typiquement :

# coding: utf-8

Le codage UTF-8 est le codage par défaut pour Python 3, il est donc inutile de l'indiquer.

Les commentaires sont introduits par le croisillon #.

On peut grouper une suite d'instructions dans un bloc. Un bloc d'instructions commence par deux-points « : » et est identé, c'est-à-dire qu'il a une marge constituée de quatre espaces — on peut aussi utiliser une tabulation mais il ne faut pas mélanger les deux méthodes ; les tabulations sont déconseillées, il vaut mieux utiliser quatre espaces[1]. Pour terminer le bloc, il suffit simplement de revenir en début de ligne ; contrairement à d'autres langages, il n'y a pas de commende de fin (end), c'est l'indentation qui définit le bloc.

: # début du bloc
    instruction 1
    instruction 2dernière instruction du bloc
instruction hors bloc

Par exemple, une exécution conditionnelle if ou une boucle for exécute un bloc d'instruction. Si l'on a besoin d'un bloc d'instruction qui « ne fait rien », on utilise l'instruction pass.

Structures de contrôle[modifier | modifier le wikicode]

Boucle itérative

La boucle itérative s'écrit :

for <variable> in <itérable>:
    <bloc d'instructions>

Si l'on veut que la variable prenne n valeurs de 0 à n – 1, on utilise l'instruction range() :

for i in range(5):
    print(i)
print("Fin de la boucle")

[▶]

0
1
2
3
4
Fin de la boucle

En fait, la commande range() extrait des valeurs de l'ensemble des nombres entiers ; on peut ainsi utiliser le découpage en tranches, par exemple range(2, 5)pour avoir la « liste » [2, 3, 4]. Notez que range() ne crée pas à proprement parler une liste, cela crée un objet de type « range » (plage, intervalle) ; pour avoir une liste, il faut écrire list(range(n)).

Dans une boucle, la commande continue() saute la fin du bloc d'instruction et passe à la valeur suivante de la boucle. La commande break() interrompt la boucle et passe à la suite.

Exécution conditionnelle

L'exécution conditionnelle s'écrit :

if <booléen>:
    <bloc d'instructions>

On peut utiliser les commandes elif (else if) et else :

if <booléen>:
    <bloc d'instructions>
elif <booléen>:
    <bloc d'instructions>
else:
    <bloc d'instructions>

Boucle antéconditionnée

La boucle antéconditionnée s'écrit :

while <booléen>:
    <bloc d'instructions>

Cette boucle peut contenir des instructions continue() et break().

Fonction[modifier | modifier le wikicode]

La déclaration d'une fonction utilise la commande def. La fonction est un bloc d'instructions. Si elle doit renvoyer des valeurs, on utilise la commande return. Par exemple

def nombres(n):
    """Entrer plusieurs nombres""" # description de la fonction
    foo = [] # initialisation
    for i in range(n):
        foo = foo+[float(input("Entrez un nombre"))]
    return foo
a = nombres(3)
print(a)

La fonction commence par une chaîne de caractères qui la décrit. Cette chaîne peut être récupérée automatiquement par certains logiciels pour faire une documentation automatique. Si la description prend plusieurs lignes, elle commence et finit par trois double-guillemets """…""" ; en fait, par convention, même si cela n'est pas obligatoire, les descriptions sont toutes encadrées de trois double-guillemets. Cette description est appelée docstring (documentation string). Pour récupérer les docstrings :

def foo():
    """Cette fonction ne fait rien"""
    
    pass

print(foo.__doc__)
# Cette fonction ne fait rien

L'instruction input() permet à l'utilisateur de saisir une valeur. La valeur est retournée sous la forme d'une chaîne de caractères qui est ensuite convertie en nombre réel avec l'instruction float().

On peut définir une valeur par défaut en l'indiquant dans l'en-tête de la définition de la fonction, de la manière suivante :

def nombres(n=1): # valeur par défaut : 1
    """Entrer plusieurs nombres"""
    foo = [] # initialisation
    for i in range(n):
        foo = foo+[float(input("Entrez un nombre"))]
    return foo

Si le paramètre à initialiser est de type modifiable (mutable), comme par exemple une liste, il faut procéder comme suit :

def fooFonction(fooListe=None): # valeur par défaut : n'existe pas
    """Description"""
    if fooListe = None:
        fooListe = [] # initialisation
    <suite des instructions>

Par défaut, les variables sont locales. On peut rendre une variable globale avec l'instruction global à l'intérieur de la fonction, avant l'utilisation de la variable. Par exemple :

a = 1
b = 1

def toto():
    """Test de variable globale"""
    global a
    a = 2
    b = 2

toto()
print("a =", a, "; b =", b) # a = 2 ; b = 1

Pour être plus précis : si une variable n'est pas assignée dans une fonction, alors Python va chercher une variable du même nom à l'extérieur de la fonction. Mais à partir du moment où la variable est assignée dans la fonction, elle devient locale sauf si l'on a utilisé l'instruction global.

Si l'on s'attend à un nombre indéfini d'arguments, on utilise la notion d'empaquetage/dépaquetage (packing/unpacking)[2]. L'empaquetage consiste à mettre les arguments dans un n-uplet, le dépaquetage consiste à développer un n-uplet en plusieurs variables. Cela se fait en mettant un astérisque (splat) « * » devant le nom de la variable. Par convention, on utilise le nom de variable *args mais cela n'est pas obligatoire.

def concatenation(*args):
    """Concatène des chaînes de caractères"""
    resultat = ""
    for i in args:
        resultat = resultat + i
    return resultat

concatenation("a", "foo", "toto") # 'afoototo'

À l'inverse, si une fonction doit recevoir plusieurs paramètres, on peut à la place lui transmettre une liste à dépaqueter :

def addition(a, b):
    """Ajoute deux nombres"""
    return a+b

arg = (1, 2)
addition(*arg) # 3

On peut aussi empaqueter/dépaqueter un dictionnaire, on utilise pour cela deux astérisques « ** ». Par convention, on utilise le nom **kwargs sans que cela ne soit obligatoire.

L'instruction lambda permet de créer de petites fonctions ne contenant pas de boucle ni de branchement conditionnel. Cependant, si la déclaration est courte et compacte, le code n'est pas toujours facilement lisible ; l'utilisation de cette instruction n'est pas recommandé.

Par exemple l'expression

f = lambda x: 2*x

est la même chose que

def f(x):
    return 2*x
 l'instruction eval() exécute une chaîne de caractères c'est-à-dire traite une chaîne de caractères comme si c'étaient des instructions données à Python. Cette instruction est à éviter pour deux raisons :
  1. Un utilisateur malveillant pourrait entrer du code malveillant dans la chaîne de caractères.
  2. L'exécution est lente puisque Python doit compiler la chaîne à la volée.
Cette instruction peut en général être remplacée par une autre instruction.

Gestion des erreurs[modifier | modifier le wikicode]

Dans un bloc d'instructions, on peut utiliser la structure try:… except:. Le bloc après try est exécuté ; si une erreur se déclare dans ce bloc, alors le bloc except s'exécute. Par exemple

try:
    1/0 # Génère une erreur
except:
    print("Division par zéro") # Cette instruction est donc exécutée

On peut compléter avec else: et finally: :

try:
    <code à exécuter>
except:
    <sexécute en cas derreur>
else:
    <sexécute sil ny a pas derreur>
finally:
    <sexécute dans tous les cas>

On peut séparer les différents types d'erreur :

try:
    <code à exécuter>
except ValueError:
    print("Valeur erronée")
except TypeError:
    print("Type erroné")

Les types d'erreur les plus courants sont :

  • NameError : le nom de variable n'existe pas ;
  • TypeError : la valeur n'est pas du bon type ;
  • ValueError : la valeur n'est pas compatible avec ce qui est attendu ;
  • RuntimeError : type d'erreur général.

On peut aussi créer ses propres erreurs : si une situation erronée survient, on peut « lever » une exception avec raise. Par exemple

if a < 0:
    raise ValueError("La valeur doit être positive")
Ressources

Exercices[modifier | modifier le wikicode]

Calcul du PGCD et du PPCM par l'algorithme d'Euclide[modifier | modifier le wikicode]

Pour plus de détails voir : w:Algorithme d'Euclide.

Écrire un programme Python qui demande deux nombres entiers et affiche leurs PGCD et PPCM. Le programme utilisera l'algorithme d'Euclide.

Notez que le module NumPy propose l'instruction gcd() :

import numpy

print(numpy.gcd(a, b))

Tours de Hanoï[modifier | modifier le wikicode]

Pour plus de détails voir : w:Tours de Hanoï.

Écrire un programme Python qui demande le nombre n de plateaux et affiche les manipulations nécessaires pour déplacer la pile d'un emplacement à un autre. Le programme utilisera l'algorithme récursif.

Mesurer le temps[modifier | modifier le wikicode]

Le module time fournit les fonctions suivantes :

  • time.gmtime() : renvoie la date et l'heure du méridien de Greenwich (Greenwich mean time, GMT), sous la forme d'un dictionnaire (année, mois, jour du mois, heure, minute, seconde, jour de la semaine, jour de l'année, heure d'été/hiver),
    • jour de la semaine est un entier entre 0 (lundi) et 6 (dimanche),
    • jour du mois est un entier entre 1 et 366 ;
  • time.localtime() : comme le précédent, mais l'heure est l'heure locale ;
  • time.time() : donne le nombre de seconde qui se sont écoulées depuis le 1er janvier 1970 ;
  • time.gmtime(n) et time.localtime(n) transforment un nombre de secondes (écoulées depuis le 1er janvier 1970) en une date au format (année, mois, jour, etc.), n-uplet de neuf valeurs ; time.mktime() fait le contraire, il transforme un n-uplet de neuf valeurs (années, mois, jour, etc.) en un nombre de secondes (écoulées depuis le 1er janvier 1970) ;
  • time.sleep(n) : provoque une pause dans le déroulement du programme de n secondes ;
  • time.perf_counter() : indique une date en seconde ; s'utilise pour mesurer la durée d'exécution d'une partie du code, en faisant la différence entre deux relevés.

Concernant la date et l'heure sous la forme d'un n-uplet, on peut extraire l'heure de la manière suivante :

import time
a = time.localtime()

print("Il est ", a[3], "h", a[4])
# ou bien
print("Il est ", a.tm_hour, "h", a.tm_min)

Pour mesurer la performance d'une portion de code :

import time

t1 = time.perf_counter()
<suite dinstructions>
t2 = time.perf_counter()

print("Durée d'exécution :", t2-t1

Programmation orientée objet[modifier | modifier le wikicode]

Nous n'allons pas ici faire un cours de programmation orientée objet (POO), nous allons aborder le sujet de manière pragmatique.

De manière schématique, un « objet » est une « super-variable ». Cette super-variable peut contenir plusieurs variables, appelées « attributs » ; elle contient en fait un dictionnaire (paires « nom d'attribut : valeur d'attribut »). Elle peut aussi contenir des fonctions spécifiques appelées « méthodes ». De même qu'une variable a un type, un objet fait partie d'une « classe ». La classe est le modèle de l'objet ; en franglais informatique, on dit que l'objet est une instance de la classe.

La POO est donc un formalisme : lorsque l'on définit des variables et des fonctions concernant un même type d'objet (au sens commun du terme), on les empaquette dans une classe. Il faut donc d'abord définir la classe, puis attribuer cette classe à un objet (« instancier » la classe).

Considérons par exemple que nous voulons travailler sur des engrenages ; pour simplifier, nous nous contentons d'engrenages à dentures droites. Une roue dentée, un pignon, est essentiellement définie par son nombre de dents Z et par son module m qui correspond à la largeur de dents[3]. Nous allons définir trois méthodes : la méthode .diametrePrimitif() qui calcule le diamètre primitif de la roue dentée, .pas() qui calcule la largeur des dents au niveau du cercle primitif et .rapport() qui calcule le rapport de transmission de deux roues engrenées Z1/Z2. La méthode .rapport() vérifie par ailleurs que les roues ont le même module, condition indispensable pour former un engrenage.

Nous définissons la classe ainsi :

class pignon:
    """roue dentée""" # explication de la classe
    
    pi = 3.141592653589793 # pour calculer le pas
    
    def __init__(self, Z=13, m=0.06):
        # instructions lancées lors de la déclaration
        """Valeurs des attributs"""
        self.Z = Z # nombre de dents
        self.m = m # module
    
    def diametrePrimitif(self):
        """Calcule le diamètre primitif"""
        return self.m*self.Z
    
    def pas(self):
        """Calcule le pas"""
        return self.pi*self.m
    
    def rapport(roueDentee, self):
        """Calcule le rapport de transmission"""
        if roueDentee.m != self.m: # gestion de l'erreur
            raise ValueError("Les pignons doivent avoir le même module")
        else:
            return roueDentee.Z/self.Z

Nous remarquons que lorsque nous déclarons les méthodes, le paramètre self correspond à l'objet lui-même. Ainsi, dans la méthode .rapport(), la variable self.Z est le nombre de dents de la roue elle-même et roueDentee.Z est le nombre de dents de la roue passée en paramètre.

Pour déclarer les roues, nous écrivons :

roue1 = pignon() # attribution de la classe, « instanciation »
roue1.Z = 13 # définition des caractéristiques du pignon « roue1 »
roue1.m = 2

roue2 = pignon(16, 2) # manière alternative

Nous pouvons alors utiliser les objets de la manière suivante :

print(roue1.Z) # 13
print(roue1.diametrePrimitif()) # 26
R = roue1.rapport(roue2) # 0.8125

La commande dir(a) affiche tous les attributs et méthodes de l'objet a.

Ressources
Classes sur Python documentation. Consulté le 2019-03-08

Interface graphique avec Tk[modifier | modifier le wikicode]

Une interface graphique utilisateur (GUI, graphic user interface) est un ensemble de boîtes permettant d'interagir avec l'utilisateur c'est-à-dire permettre la saisie d'informations, l'exécution d'actions et afficher des informations. Elle se compose d'éléments appelés widgets.

Les éléments (widgets) classiques sont :

  • boîte de dialogue (dialog box) : fenêtre contenant d'autres éléments ;
  • étiquette (label) : texte affiché ;
  • liste déroulante (drop-down list) : zone permettant le choix d'une option, la liste se déployant lorsque l'on clique sur la zone ;
  • zone de texte, champ de saisie (text box) : zone permettant de taper du texte ;
  • boîte combinée (combo box) : zone de saisie de texte contenant une liste déroulante qui permet de choisir des éléments prédéfinis ;
  • bouton (button) : objet effectuant une action lorsque l'on clique dessus ;
  • case à cocher (checkbox, tickbox) : objet permettant d'activer ou de désactiver une option lorsque l'on clique dessus ;
  • bouton radio, case d'option (radio button) : objet permettant d'activer une option en désactivant les autres options ; une seule option peut être activée à la fois.

Plusieurs modules permettent de gérer les interfaces graphiques. Nous choisissons ici le module développé sur la bibliothèque Tk qui est une bibliothèque multiplateforme. Pour cela, nous importons le module tkinter ainsi que le module ttk, ce dernier proposant des options plus « modernes » :

import tkinter as tk
from tkinter import ttk

Voici un programme permettant comme précédemment de calculer le rapport de transmission d'un engrenage. Nous détaillons sa construction ci-après.

# référence : https://tkdocs.com/tutorial/firstexample.html

import tkinter as tk
from tkinter import ttk

# ***************
# ***************
# ** Fonctions **
# ***************
# ***************

def calcule(*args): 
    """Calcule le rapport de transmission d'un engrenage"""
    
    try:
        valeurZ1 = float(IUz1.get())
        valeurM1 = float(IUm1.get())
        valeurZ2 = float(IUz2.get())
        valeurM2 = float(IUm2.get())
        if valeurM1 != valeurM2:
            IUrapport.set("Erreur de module")
        else:
            IUrapport.set(valeurZ2/valeurZ1)
    except:
        IUrapport.set("erreur")

# *************************
# *************************
# ** Interface graphique **
# *************************
# *************************

# fenetre principale
fenetre = tk.Tk()
fenetre.title("Rapport de réduction")

# élément (widget) cadre contenant tout le reste
cadre = ttk.Frame(fenetre, padding="3 3 12 12")
cadre.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S))

# le cadre s'étire si l'on étire la fenêtre
fenetre.columnconfigure(0, weight=1)
fenetre.rowconfigure(0, weight=1)

# Paramètres du système (variables)
IUz1 = tk.StringVar()
IUm1 = tk.StringVar()
IUz2 = tk.StringVar()
IUm2 = tk.StringVar()
IUrapport = tk.StringVar()

# Création des zones de saisie
z1_entry = ttk.Entry(cadre, width=7, textvariable=IUz1)
m1_entry = ttk.Entry(cadre, width=7, textvariable=IUm1)
z2_entry = ttk.Entry(cadre, width=7, textvariable=IUz2)
m2_entry = ttk.Entry(cadre, width=7, textvariable=IUm2)

# Création des étiquettes statiques
z1_label = ttk.Label(cadre, text="z1")
m1_label = ttk.Label(cadre, text="m1")
z2_label = ttk.Label(cadre, text="z2")
m2_label = ttk.Label(cadre, text="m2")
rapport_statique = ttk.Label(cadre, text="Rapport de transmission : ")

# Création de l'étiquette dynamique
rapport_dynamique = ttk.Label(cadre, textvariable=IUrapport)

# Création du bouton
bouton = ttk.Button(cadre, text="Calcul", command=calcule)

# Placement des éléments (widgets)
z1_label.grid(column=1, row=1, sticky=tk.W)
z1_entry.grid(column=2, row=1, sticky=(tk.W, tk.E))

m1_label.grid(column=1, row=2, sticky=tk.W)
m1_entry.grid(column=2, row=2, sticky=(tk.W, tk.E))

z2_label.grid(column=1, row=3, sticky=tk.W)
z2_entry.grid(column=2, row=3, sticky=(tk.W, tk.E))

m2_label.grid(column=1, row=4, sticky=tk.W)
m2_entry.grid(column=2, row=4, sticky=(tk.W, tk.E))

rapport_statique.grid(column=1, row=5, sticky=tk.W)

rapport_dynamique.grid(column=2, row=5, sticky=(tk.W, tk.E))

bouton.grid(column=2, row=6, sticky=tk.W)

# ajoute une gouttière entre les éléments
for enfant in cadre.winfo_children():
    enfant.grid_configure(padx=5, pady=5)

# Emplacement initial du curseur
z1_entry.focus()

# effet de la touche [entrée]
fenetre.bind("<Return>", calcule)

# *************************
# *************************
# ** Programme principal **
# *************************
# *************************

# Affichage et activation de la fenêtre
fenetre.mainloop()

Explications

Nous commençons par définir la boîte de dialogue que nous appelons fenetre ; c'est un objet Tk et nous lui donnons un titre «  » :

fenetre = tk.Tk()
fenetre.title("Rapport de réduction")

Puis, nous définissons un cadre attaché à cette fenêtre et qui va nous permettre « d'accrocher » les autres éléments, ce qui permet de garder une apparence satisfaisante lorsque l'on retaille la fenêtre :

cadre = ttk.Frame(fenetre)

Le cadre va comporter six lignes (row) et deux colonnes (column).

Nous allons placer une étiquette (label) « z1 » : text="z1". Cette étiquette se trouve dans une case du cadre, celle de la première colonne et la première ligne : grid(column=1, row=1). Par rapport à cette case, elle est collée à « l'ouest » (W, west, gauche) de la case : sticky=tk.W.

z1_label = ttk.Label(cadre, text="z1") # Création de l'étiquette
z1_label.grid(column=1, row=1, sticky=tk.W) # Placement de l'étiquette

Notez que l'on aurait pu écrire directement :

ttk.Label(cadre, text="z1").grid(column=1, row=1, sticky=tk.W)

mais le fait de séparer la création de l'élément et son placement facilite la maintenance (recherche d'erreur, évolution du code).

Pour tout ce qui est dynamique, c'est-à-dire les zone de saisie des valeurs et l'affichage du résultat, il faut définir des « chaînes variables » () :

IUz1 = tk.StringVar()

Cette variable est une variable globale à la création. Nous pouvons alors placer la zone de saisie (entry) à côté de l'étiquette lui correspondant. Nous nommons la zone de saisie z1_entry :

z1_entry = ttk.Entry(cadre, width=7, textvariable=IUz1)

Nous faisons de même pour les trois autres paramètres de l'engrenage, m1, z2 et m2. Le résultat est également une chaîne variable globale. Par rapport à notre mise en page, elle se situe dans la case colonne 2 ligne 5, centrée sur cette case (collé à l'est et à l'ouest) :

rapport = tk.StringVar()
rapport_dynamique = ttk.Label(cadre, textvariable=rapport)
rapport_dynamique.grid(column=2, row=5, sticky=(tk.W, tk.E))

Il nous faut encore définir une fonction de manière classique, nous l'appelons « calcule ». Les variables étant globales, on les utilise directement. On récupère les valeurs avec la méthode get() et nous modifions la valeur avec la méthode set() :

def calcule():
    valeurZ1 = float(IUz1.get())
    valeurZ2 = float(IUz2.get())
    IUrapport.set(valeurZ2/valeurZ1)
Cette fonction est déclenchée lorsque l'on clique sur le bouton « Calcul » situé dans la case du cadre ligne 6 colonne 2 :
bouton = ttk.Button(cadre, text="Calcul", command=calcule)
bouton.grid(column=2, row=6, sticky=tk.W)

ou bien si l'on appuie sur la touche [entrée] du clavier :

fenetre.bind("<Return>", calcule)

À tout ceci, nous ajoutons des « gouttières » (marges, paddings) afin d'espacer les éléments.

Il faut ensuite « activer » la fenêtre pour qu'elle s'affiche. La méthode est mainloop() (boucle principale) : « boucle » (elle est active en permanence et attend des actions sur ses éléments),

fenetre.mainloop()

Nous avons ci-dessus mis la plupart du code en programme principal. Nous pouvons aussi programmer de manière fonctionnelle, en mettant la plupart du code dans des fonctions ; cependant, pour que la fenêtre et les variables dynamiques soient globales à tout le programme, elles doivent être déclarées dans le programme principal. Nous pouvons aussi mêler la programmation orientée objet.

Annotations[modifier | modifier le wikicode]

Une annotation est un commentaire qui sert à expliciter un type de variable. La syntaxe est différente des commentaires « classiques » : cela permet d'avoir un affichage différent avec les éditeurs de texte ayant une coloration syntaxique, et ces informations peuvent être récupérées par des logiciels extérieurs pour effectuer une documentation automatique ou bien des vérifications de type. Cependant :

  • comme les commentaires normaux, ils n'ont aucune influence lors de l'exécution du texte ; en particulier :
  • rien n'oblige à annoter les variables ;
  • il est possible d'avoir une variable ayant un type différent de son annotation ; le fait de pouvoir définir et changer le type de variable à la volée est une fonctionnalité fondamentale de Python.

La syntaxe pour une annotation est :

nom_de_variable + deux-points + espace + type

par exemple :

a: int

Notez qu'ici, la variable n'est pas créée. Pour la créer, il faut lui affecter une valeur. Il est possible de l'affecter après ou bien sur la même ligne avec la syntaxe :

nom_de_variable + deux-points + espace + type + espace + égal + espace + valeur

par exemple :

a: int
a = 5
# est équivalent à
a: int = 5

Même si l'annotation n'a pas d'impact sur l'exécution, le type doit être un type existant sinon cela génère une erreur de syntaxe. Les types classiques sont :

intfloatstrboollisttupledict

Il est également possible de mettre une chaîne de caractères :

a: "ce que je veux" = 3.1516

On peut annoter une fonction. Il est possible d'annoter les variables déclarées au sein de la fonction, mais pas les variables globales (puisqu'elle ne sont pas définie au sein de la fonction). On peut aussi annoter :

  • les variables passées en paramètre, avec la même syntaxe dans les parenthèses ;
  • annoter le type de la variable de sortie (retournée) en la faisant précéder de -> :
def plusCinq(a: float = 0) -> float:
    return a + 5
Ressources

Décoration[modifier | modifier le wikicode]

Une décoration est une fonction qui s'applique à une fonction, à la manière de la composition mathématique g ∘ ƒ = g(ƒ). Mais cette composition affecte la fonction elle-même ; l'utilisateur appelle la fonction ƒ mais c'est la fonction g ∘ ƒ qui s'exécute. Cette fonction g est appelée la décoration.

L'intérêt est de pouvoir modifier une fonction sans modifier le code de la fonction elle-même.

Pour appliquer une décoration, il faut :

  1. Déclarer la décoration : une fonction qui s'applique à une autre fonction.
  2. Affecter la décoration à la fonction visée : en mettant @décoration juste avant la définition de la fonction.

Par exemple :

def decoration(f):
    print("Avant la fonction")
    f()
    print("après la fonction")

@decoration
def afficheFoo():
    print("Foo.")

foo()

# Avant la fonction
# Foo.
# Après la fonction

Lorsque l'on appelle foo(), on appelle en fait decoration(foo()).

Si la fonction à modifier admet des paramètres, il faut définir une fonction enveloppante dans la décoration. Par exemple, nous définissons ci-dessous une décoration deuxFois() qui fait s'exécuter deux fois de suite la fonction :

def deuxFois(f):
    def conteneurFonction(*args, **kwargs):
        f(*args, **kwargs)
        f(*args, **kwargs)
    return  conteneurFonction

@deuxFois
def plusCinq(a: int = 0):
    print(a + 5)

plusCinq(2)
# 7
# 7

print(plusCinq.__name__)
# conteneurFonction

Nous voyons que l'application de la décoration a modifié le nom de la fonction — pas le nom de la variable qui contient la fonction mais bien son nom « intime ». Pour éviter cela,on utilise la méthode wraps() du module functools :

import functools

def deuxFois(f):
    @functools.wraps(f)
    def conteneurFonction(*args, **kwargs):
        f(*args, **kwargs)
        f(*args, **kwargs)
    return  conteneurFonction

@deuxFois
def plusCinq(a: int = 0):
    print(a + 5)

plusCinq(2)
# 7
# 7

print(plusCinq.__name__)
# plusCinq
Ressources
(en) PEP 318 -- Decorators for Functions and Methods sur Python.org. Consulté le 2019-04-05

Exporter un programme Python[modifier | modifier le wikicode]

Vous pouvez créer un fichier « Python pur » .py. Pour cela, dans le menu fichier/file de Jupyter, choisir télécharger/download au format .py ; le fichier se trouve alors dans le répertoire de téléchargement du navigateur.

Ressources[modifier | modifier le wikicode]

Notes et références[modifier | modifier le wikicode]

  1. Tabs or Spaces? sur Python documentation. Consulté le 2019-03-14
  2. Introduction à *args et **kwargs sur Developpez.com. Consulté le 2019-03-09.
  3. ainsi que par son épaisseur e et le matériau dont elle est faite mais nous allons négliger ces paramètres pour la simplicité de l'étude.

Fonctions mathématiques générales < > Graphiques