Python pour le calcul scientifique/Éléments de programmation
Rappel : les programmes commencent par :
#!/usr/bin/python3
import numpy as np
import matplotlib.pyplot as plt
Entrées et sorties
modifierPour permettre à l'utilisateur ou à l'utilisatrice d'entrer une valeur, nous utilisons la fonction input()
comme évoqué précédemment (chapitre Premiers programmes), avec la syntaxe variable = input(texte)
. Notez que la valeur renvoyée par input()
est une chaîne de caractères. Si vous voulez autre chose, typiquement un nombre, il faut convertir cette chaîne.
Par exemple, nous demandons ici d'entrer une longueur sous la forme d'une valeur numérique :
longueurDefaut = 10.0
texteDemandeLongueur = f"Veuillez entrer la longueur en millimètres (valeur par défaut {longueurDefaut} mm) : "
longueur = input(texteDemandeLongueur)
if longueur=="":
longueur=longueurDefaut
else:
longueur=float(longueur)
print(longueur)
Pour afficher un texte, on utilise la fonction print()
, également présentée dans le chapitre Premiers programmes, avec la syntaxe print(texte)
. Le texte à afficher peut être de n'importe quel type (entier, réel en virgule flottante, booléen, chaîne de caractères…) mais si l'on veut « mélanger » les types, il faut tout convertir en chaînes de caractères, avec la fonction str()
, et concaténer les chaînes avec +
.
Par exemple :
print("La longueur vaut : "+str(longueur)+" mm.")
Nous pouvons aussi utiliser une « chaîne “f” » (f-string) : on met un le f
devant le guillemet ouvrant et dans la chaîne, on met un champ sous la forme {nomDeVariable}
. L'exemple ci-dessus devient alors :
print(f"La longueur vaut : {longueur} mm.")
Les chaînes « f » sont détaillées dans la section Chaînes de caractères ci-dessous.
Si l'on veut introduire un retour à la ligne dans la chaîne, on utilise les caractères \n
(contre-oblique suivie de la lettre N minuscule). Par exemple
print("Ceci est un texte\navec un retour à la ligne.")
Types de variables
modifierGénéralités
modifierPython 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, avec le module NumPy
:
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] estTrue
six[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
modifierEntiers
modifierNous 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
modifierLes réels disposent de fonctions spécifiques appelées « méthodes ».
Une méthode est une 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
modifierNous 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- Ressources
- « 7. Input and Output », sur Python Documentation (consulté le 6 avril 2019)
Généralités
modifierIl 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"…"
our'…'
: 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"…"
ouf'…'
: 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
modifierLorsque 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
modifierLe 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 avecstr.center(n, c)
, par exemple"a".center(7, ".")
donne"....a...."
;str.ljust(n, c)
etstr.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
modifierLa fonction chr()
transforme un code Unicode en caractère. Par exemple, chr(97)
donne "a"
et chr(0x03c0)
donne "π"
.
Si on veut créer une liste de caractères qui se suivent, on peut par exemple utiliser :
[chr(x) for x in range(97, 102)]
# ['a', 'b', 'c', 'd', 'e']
Si on veut créer une liste de nombres sous la forme de chaînes de caractères, on peut utiliser la commande str()
vue ci-dessus. Par exemple :
[str(x) for x in range(1, 6)]
# ['1', '2', '3', '4', '5']
Pour la syntaxe, voir ci-dessous la section Définition en compréhension.
Manipulation de listes
modifierLes listes sont une structure de données fondamentale en Python.
- Ressources
- (en) « 5. Data structures », sur Python documentation (consulté le 16 mars 2019)
Copie d'une liste
modifierContrairement à 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
modifierPour modifier une liste, vous disposez des méthodes suivantes :
a.append(x)
: ajoute l'élémentx
à la fin de la listea
;a.extend(x)
: ajoute la listex
à la fin de la listea
;a.append(i, x)
: aoute l'élémentx
avant l'interstice i de la listea
;x = a.pop(i)
: enlève l'élément i de la listea
et le met dans la variablex
;x = a.pop()
enlève le dernier élément de la liste ;a.clear()
: vide la listea
;a.sort()
: trie la liste par ordre croissant ;a.sort(reverse = True)
: trie par ordre décroissant ;a.reverse()
: inverse l'ordre dea
.
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 valeurx
de la listea
;a.index(x)
: indique l'indice où se trouve la première occurrence de la valeurx
;a.count(x)
: indique le nombre de fois que l'on trouve la valeurx
dans la listea
.
Définition en compréhension
modifierLa 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
modifierUn 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 2 … derniè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
modifierBoucle 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
modifierLa 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
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 :
- Un utilisateur malveillant pourrait entrer du code malveillant dans la chaîne de caractères.
- 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
modifierDans 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:
<s’exécute en cas d’erreur>
else:
<s’exécute s’il n’y a pas d’erreur>
finally:
<s’exé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
- (en) « Errors and exceptions », sur Python documentation (consulté le 12 mars 2019)
- (en) « Built-in Exceptions », sur Python documentation (consulté le 12 mars 2019)
Exercices
modifierCalcul du PGCD et du PPCM par l'algorithme d'Euclide
modifier- 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.
solution
"""Programme : euclide.py
Auteur : User:cdang
date : 2019-02-19
dates de modification :
----------------------------------------------------------------------------
version de Python : 3
module requis : aucun
----------------------------------------------------------------------------
Objectif : calcule le PGCD et le PPCM de deux nombres entiers.
Entrée
------
au clavier, saisie de deux nombres entiers.
Sorties
-------
à l'écran, affichage du PGCD et du PPCM.
"""
# ***************
# ***************
# ** Fonctions **
# ***************
# ***************
def euclide():
"""Calcule le PGCD et le PPCM avec l'algorithme d'Elclide
Entrée
------
Aucune, la saisie des paramètres fait partie de la fonction
Sortie
------
affichage du PGCD et du PPCM
"""
print("***** Algorithme d'Euclide *****\n")
a0 = int(input("Premier nombre entier : a = "))
b0 = int(input("Second nombre entier : b = "))
a = a0
b = b0
r = a%b # initialisation
while (r != 0) : # algorithme d'Euclide
a = b
b = r
r = a%b
# affichage des résultats
print("PGCD(", a0, ", ", b0, ") = ", b)
print("PPCM(", a0, ", ", b0, ") = ", a0*b0//b)
# *************************
# *************************
# ** Programme principal **
# *************************
# *************************
euclide()
On peut simplifier la boucle centrale :
while b: # s'exécute tant que b n'est pas 0
a, b = b, a % b # affectation de liste à liste
return a
Notez que le module NumPy propose l'instruction gcd()
:
import numpy
…
print(numpy.gcd(a, b))
Tours de Hanoï
modifier- 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.
solution
"""nom : hanoi.py
auteur : User:cdang
date de création : 2019-02-19
dates de modification :
----------------------------------------------------------------------------
version de Python : 3
module requis : aucun
----------------------------------------------------------------------------
Objectif : résout le problème des tours de Hanoï
Entrées
-------
trois chaînes de caractères (nom des piliers)
Sorties
-------
une chaîne de caractères (liste des opérations)
"""
# ***************
# ***************
# ** Fonctions **
# ***************
# ***************
def hanoi(a, b, c, n):
"""Résout le problème des tours de Hanoï de manière récursive
But : déplace la pile de n disques du piler a au pilier b
Entrées
-------
a, b c : chaînes de 1 caractère, référence des emplacements ;
n : entier, nombre de disques sur l'emplacement a
Sorties
-------
operations : chaînes de caractères décrivant les opérations
""""
if n>1:
operations = hanoi(a, c, b, n-1)
operations = operations+a+"→"+b+" ; "
operations = operations+hanoi(c, b, a, n-1)
else:
operations = a+"→"+b+" ; "
return operations
# *************************
# *************************
# ** Programme principal **
# *************************
# *************************
resultat = hanoi("1", "2", "3", 3)
print(resultat)
Lancer de rayons
modifierConsidérons une lentille hémisphérique de rayon R faite d’un verre d’indice de réfraction n. Nous plaçons une source ponctuelle à une distance d du dioptre plan, sur l’axe optique. Tracer des rayons partant de la source et traversant la lentille.
Il s’agit d’un problème ayant une symétrie de révolution par rapport à l’axe optique. Nous pouvons nous réduire à un problème plan en nous plaçant dans un plan contenant l’axe optique ; l’axe optique est encore un axe de symétrie orthogonale, nous pouvons donc nous contenter d'étudier un demi-plan.
Pour simplifier, nous plaçons le centre du dioptre sphérique à l’origine O du repère. L’axe optique est l’axe x et l'axe perpendiculaire, vertical sur la figure, c’est l’axe y.
Les coordonnées de la source sont donc (-d ; 0). Le rayon issu de la source et faisant un angle θ avec l’axe x frappe le dioptre plan à l’altitude h. Nous avons :
- h = d ⋅ tan θ.
L’angle d’incidence vaut θ. D’après la loi de Snell-Descartes, l'angle de réfraction θ2 vaut :
- θ2 = arcsin((sin θ) / n).
Le rayon réfracté passe par le points de coordonnées (0, h). L’équation de la droite est donc :
- y = a ⋅ x + h
avec
- a = tan θ2.
L’équation du cercle de centre O et de rayon R est :
- x2 + y2 = R2.
Les coordonnées (xM, yM) de l’intersection M du rayon avec le dioptre sphérique vérifient les deux équations. Par substitution, nous obtenons une équation du second degré en x que nous savons résoudre :
- xM2 + (a ⋅ xM + h)2 = R2
- ⇔ (1 + a2) ⋅ xM2 + 2 ⋅ a ⋅ h ⋅ xM + h2 – R2 = 0.
D’après les propriétés du cercle, le rayon est perpendiculaire à la tangente. Le rayon [OM] est donc normal au dioptre en M. Nous pouvons déterminer l’angle d’incidence θi par le produit scalaire :
ce qui nous permet de calculer cet angle :
Comme nous passons vers un milieu d’indice plus faible, il y a un risque de réflexion totale. L’angle limite est :
- θmax = arcsin(1/n).
Si l’on a θi > θmax, le rayon repart vers l’intérieur. Nous ne traçons pas le rayon car cela nous emmènerait trop loin dans l’analyse. En revanche, si θi ≤ θmax, alors nous pouvons appliquer la loi de Snell-Descartes pour avoir l’angle de réfraction θe :
- θe = arcsin(n ⋅ sin θi).
Pour tracer le rayon sortant, il nous faut l’angle θ3 par rapport à l’horizontale. L’angle du rayon [OM] par rapport à l’horizontal vaut arctan(yM / xM), nous avons donc
- θ3 = arctan(yM / xM) + θe.
Structure des données
Le problème est décrit par trois paramètres :
- Le rayon
R1
de la lentille, en milliètres (réel en virgule flottante). - L’indice du verre,
n
sans dimension (réel en virgule flottante). L’indice de l’air vaut 1. - La distance de la source au dioptre d’entrée plan,
d
en millimètres (réel en virgule flottante).
Un rayon est caractérisé par quatre paramètres :
- L’angle d’émission
theta1
en radians (réel en virgule flottante). - L’angle de réfraction dans la lentille
theta2
en radians (réel en virgule flottante). - Les cordonnées
M
en millimètre (vecteur de dimension 2([x, y])
de réels en virgule flottante) du point d’intersection du rayon avec le dioptre sphérique. - L’angle de réfraction dans l’air après la lentille
theta3
en radians (réel en virgule flottante).
Pour le calcul et le tracé, nous avons besoin des paramètres intermédiaires suivants :
- l’altitude y =
h
en millimètres (réel en virgule flottante) à laquelle le rayon frappe le dioptre plan d’entrée ; - l’angle d’incidence du rayon avec le dioptre sphérique
thetaint
en radians (réel en virgule flottante).
Les angles sont stockés en radians car c’est l’unité naturelle pour le calcul mais nous affichons les valeurs en degrés. Comme le calcul de conversion est récurrent, nous conservons les facteurs degversrad
(conversion des degrés vers les radians, facteur valant π/180, réel en virgule flottante) et radversdeg
(conversion des radians vers les degrés, facteur valant 180/π, réel en virgule flottante).
Fonctions
Nous avons besoin d’une fonction qui calcule les trois paramètres du rayon (theta2, M, theta3)
à partir de l’angle d’émission theta1
. Nous appelons cette fonction lanceRayon()
. Cette fonction fait appelle à une fonction qui calcule l’angle du rayon réfracté à partir de l’angle du rayon incident theta1
, les deux angles étant par rapport à la normale au dioptre au point considéré. Nous appelons cette fonction refrac()
.
La recherche de l’intersection M
du rayon avec le dioptre sphérique nécessite de résoudre une équation du second degré. Nous utilisons pour cela la recherche des racines du polynôme en x
avec la fonction numpy.polynomial.polynomial.polyroots()
. D’après la configuration du problème géométrique, si l’on s’assure que le rayon frappe bien la lentille (0 ≤ h
≤ R1
) alors nous sommes sûrs que le problème a deux solutions réelles (une positive et une négative) ou, dans le cas dégénéré où h == R1
, une valeur unique x == 0
. Comme nous recherchons la valeur positive, nous sélectionons la plus grande des deux racines.
Pour la gestion de la réflexion interne : dans la fonction refrac()
, nous vérifions les conditions de réflexion totale et si elles sont remplies, alors nous générons une erreur (commandes try… except
et raise ValueError
). Cette erreur est propagée à la fonction lanceRayon()
: lanceRayon()
appelle la fonction refrac()
et si cette fonction renvoie une erreur, alors lanceRayon()
renvoie également une erreur.
Pour trouver l’angle d’émission thetaLimite
provoquant la réflexion totale (en radians, réel en virgule flottante), nous effectuons une recherche par dichotomie :
- nous partons de l’angle maximum possible, lorsque le rayon frappe le sommet de la lentille, et nous appelons la fonction
lanceRayon()
; si cela ne génère pas d’erreur, alors nous pouvons aller jusqu’à cette valeur, la recherche est terminée ; si cela génère une erreur, alors nous divisons la valeur par deux ; - à une étape de la recherche donnée, si
lanceRayon()
ne génère pas d’erreur avec l’angle testé, alors nous savons que l’angle limite est supérieur à cette valeur ; cette valeur minore donc la valeur recherchée ; si au contrairelanceRayon()
génère une erreur, alors c’est que l’angle est trop important, cette valeur majore donc la valeur recherchée ; nous pouvons ainsi resserer l’intervalle de recherche ; - nous nous arrêtons lorsque les valeurs haute et basse sont suffisamment proche.
Concrètement :
- Nous définissons une variable
angleHaut
angle en radians, réel en virgule flottante) qui est l’angle d’émission le plus bas connu provoquant la réflexion totale. - Nous définissons une variable
angleBas
angle en radians, réel en virgule flottante) qui est l’angle d’émission le plus haut connu ne provoquant pas de réflexion totale. Sa valeur initiale est 0. L’angle limite recherché est donc entreangleBas
etangleHaut
. - Nous définissons l’angle
angleTest
comme étant la moyenne entreangleBas
etangleHaut
. SilanceRayon(angleTest)
génère une erreur, alorsangleTest
est la nouvelle valeur d’angleHaut
(puisque c’est une valeur provoquant la réflexion totale et qu’elle est plus basse que la valeur actuelle d’angleHaut
). À l’inverse, silanceRayon(angleTest)
ne génère pas d’erreur, alorsangleTest
est la nouvelle valeur d’angleBas
(puisque c’est une valeur ne provoquant pas la réflexion totale et qu’elle est plus haute que la valeur actuelle d’angleBas
). - Nous arrêtons la procédure lorsque l’écart entre
angleBas
etangleHaut
est inférieur à 10–3 rad (valeur arbitraire).
La valeur retenue est la valeur finale d’angleBas
(puisque l’on veut être sûr qu’il n’y ait pas de réflexion totale). La valeur affichée est la valeur en degrés arrondie au dixième.
Nous demandons à l’utilisateur ou à l’utilisatrice les valeurs des paramètres du problème : rayon de la lentille, distance de la source, indice de réfraction du verre. Nous vérifions que les valeurs entrées sont bien des nombres ; si c’est une chaîne vide, alors nous utilisons une valeur par défaut.
Nous créons une fonction refrac()
qui permet de calculer l’angle réfracté à partir de l’angle d’incidence et des indices de réfraction. S’il y a rélexion totale, alors nous générons une erreur.
La fonction lanceRayon()
calcule les différents points de passage du rayon. Elle appelle pour cela la fonction refrac()
. Si un appel de la commande refrac()
génère une erreur, alors nous générons également une erreur.
Nous déterminons l’angle d’émision du rayon thetaLimite
qui provoque une réflecxion totale. Pour cela, nous créons une fonction rechercheLimite()
qui cherche par dichotomie.
Nous traçons un rayon tous les 5° jusqu’à la valeur limite.
#!/usr/bin/env python3
# coding: utf-8
"""nom : lancerRayons.py
auteur : User:cdang
date de création : 2022-05-06
dates de modification :
----------------------------------------------------------------------------
version de Python : 3
module requis : NumPy, matplotlib
----------------------------------------------------------------------------
Objectif : trace des trajets optique avec une lentille hémisphérique
Entrées
-------
Le rayon de la lentille, la distance de la source, l’indice de réfraction du verre,
trois chaînes de caractères saisies par l’utilisateur·rice et qui sont converties en réels.
Sorties
-------
La valeur limite de l’angle (réel) et le tracé de plusieurs rayons.
"""
# ******************************************************
# ******************************************************
# ** Lancer de rayons pour une lentille hémisphérique **
# ******************************************************
# ******************************************************
import numpy as np
import matplotlib.pyplot as plt
import numpy.polynomial.polynomial as nppol
# **************
# * Constantes *
# **************
# Pour la conversion degrés ↔ radians
radversdeg = 180/np.pi
degversrad = 1/radversdeg
# *************
# * Fonctions *
# *************
def boucleEntreeNombre(messageSaisie, valeurDefaut):
"""Permet de s’assurer que l’utilisateur·rice a bien entré un nombre.
Entrée :
— message à afficher (chaîne de caractères) ;
— valeur par défaut (réel à virgule flottante).
Sortie : nombre (réel à virgule flottante)."""
messageErreur = "Veuillez entrer une valeur numérique (ou vide pour accepter la valeur par défaut).\n"
execute = True
while execute:
strNombre = input(messageSaisie+f" (valeur par défaut {valeurDefaut}) : ")
if strNombre == "":
nombre = valeurDefaut
execute = False
else:
try:
nombre = float(strNombre)
except:
print(messageErreur)
else:
execute = False
return nombre
def initialisation():
"""L’utilisateur·rice entre les variables du problème.
Entrées : aucune.
Sorties :
— R1 (mm) : rayon de la lentille ;
— d (mm) : distance de la source au dioptre plan ;
— n (sans dimension) : indice de réfraction du verre."""
R1 = boucleEntreeNombre("Rayon de la lentille en mm", 20.0)
d = boucleEntreeNombre("Distance de la source au dioptre plan en mm", 20.0)
n = boucleEntreeNombre("Indice de réfraction (sans dimension)", 1.5)
return (R1, d, n)
def refrac(n1, n2, theta1):
"""Calcule l’angle de réfraction theta2 (radians) en fonction
— de l’angle d’incidence theta1 (radians);
— de l’indice de réfraction n1 du premier milieu ;
— de l’indice de réfraction n2 du second milieu."""
reflexionTotale=False
rapport=n2/n1
rapportinv=np.reciprocal(rapport)
if n1 > n2:
thetal = np.arcsin(rapport) # angle limite pour la réflexion totale
if theta1 >= thetal:
reflexionTotale=True
if reflexionTotale:
print("Réflexion totale")
raise ValueError
else:
return np.arcsin(rapportinv*np.sin(theta1))
def lanceRayon(n1, n2, d, R, theta1):
"""Détermine le rayon issu de la source
située à une distance d (mm) du bareau
et avec une élévation de theta1 (radians),
en fonction des indices de réfraction n1 et n2.
Les éléments retournés sont :
— la hauteur h (mm) à laquelle le rayon frappe le barreau ;
— l’angle de réfraction theta2 (radians)) dans le barreau ;
— l’angle de réfraction theta3 (radians) à la sortie du barreau
— le point M(x, y) (mm) auquel le rayon sort du barreau."""
h = d*np.tan(theta1)
if h >= R:
print("Le rayon est au-dessus du barreau")
raise ValueError
else:
theta2 = refrac(n1, n2, theta1)
a = np.tan(theta2)
x = max(nppol.polyroots([h*h - R*R, 2*a*h, 1+a*a])) # recherche de l’intersection du rayon avec le cercle
y = a*x + h
M = np.array([x, y])
thetaint = np.arccos((x + a*y)/(R*np.sqrt(1 + a*a)))
theta3 = np.arctan(y/x) - refrac(n2, n1, thetaint)
return (h, theta2, theta3, M)
def rechercheLimite(n1, n2, d, R):
"""Recherche l’angle limite pour la réflexion totale.
Entrée :
— indice de réfraction des milieux 1 et 2, n1 et n2 ;
— distance au barreau, d(mm).
Sortie : angle limite theta (radians)"""
angleHaut = np.arctan(R/d)
angleBas = 0
angleTest = angleHaut
try:
lanceRayon(n1, n2, d, angleTest, R)
except:
condition = True # il y a réflexion total en haut de la lentille
else:
condition = False # il n’y a jamais réflexion totale dans la lentille
while condition: #dichotomie
angleTest = np.mean([angleHaut, angleBas]) # on ajuste la valeur de test
try:
lanceRayon(n1, n2, d, R, angleTest)
except:
angleHaut = angleTest # réflexion totale : on abaisse la valeur maximale
else:
angleBas = angleTest # pas de réflexion totale : on monte la valeur minimale
condition = ((angleHaut - angleBas) >= 0.001) # on a cerné la limite à 0,001 rad près
if not condition:
angleTest = angleBas
return angleTest
# ***********************
# * Programme principal *
# ***********************
(R1, d, n) = initialisation()
xmax = round(R1 + d)
thetaLimite = rechercheLimite(1, n, d, R1)
thetaLimiteDeg = thetaLimite*radversdeg
print(f"Angle limite pour la réflexion totale : {thetaLimiteDeg:.1f}°.\n")
anglesDeg = np.arange(0, thetaLimiteDeg, 5)[1:] # trace un rayon tous les 5°
anglesRad = anglesDeg*degversrad
nb = len(anglesDeg)
h = np.zeros(nb) # initialisation des vecteurs de valeurs
theta2 = np.zeros(nb)
theta3 = np.zeros(nb)
M = np.zeros((nb, 2))
for i in range(nb):
(h[i], theta2[i], theta3[i], M[i, :]) = lanceRayon(1, n, d, R1, anglesRad[i])
(h_lim, theta2_lim, theta3_lim, M_lim) = lanceRayon(1, n, d, R1, thetaLimite)
# tracé
anglesCercle = 0.5*np.pi*(np.linspace(1, 0, 20))
x_cercle = R1*np.cos(anglesCercle) # coordonnées des pints du cercle
y_cercle = R1*np.sin(anglesCercle)
fig = plt.plot([-d,xmax], [0, 0], "k-.", linewidth="0.5") # tracé de l’axe optique
for i in range(nb):
plt.plot([-d, 0, M[i, 0], xmax], [0, h[i], M[i, 1], M[i, 1] + (xmax - M[i, 0])*np.tan(theta3[i])],
label=f"{anglesDeg[i]:.0f}°")
plt.plot([-d, 0, M_lim[0], xmax], [0, h_lim, M_lim[1], M_lim[1] + (xmax - M_lim[0])*np.tan(theta3_lim)],
label=f"{0.1*int(np.trunc(10*thetaLimite*radversdeg)):.1f}°")
plt.plot(x_cercle, y_cercle, "k", linewidth="0.5") # tracé du cercle
plt.plot([0,0], [0, R1], "k", linewidth="0.5") # tracé du premier dioptre
#plt.axis("square")
plt.gca().set_aspect("equal", adjustable="box")
plt.xlabel("x (mm)")
plt.ylabel("y (mm)")
plt.title("Lentille hémisphérique, lancer de rayons")
plt.legend()
plt.savefig("lentille_hemispherique_lancer_rayon.svg", format="svg")
plt.show()
Mesurer le temps
modifierLe 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)
ettime.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 d’instructions>
t2 = time.perf_counter()
print("Durée d'exécution :", t2-t1
Programmation orientée objet
modifierNous 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 8 mars 2019)
Interface graphique avec Tk
modifierUne 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.
Calcul du rapport de transmission en programmation fonctionnelle et orientée objet
# référence : https://tkdocs.com/tutorial/firstexample.html
import tkinter as tk
from tkinter import ttk
# *************
# *************
# ** Classes **
# *************
# *************
class pignon:
"""roue dentée""" # explication de la classe
pi = 3.141592653589793 # pour calculer le pas
def __init__(self, Z=13, m=0.06):
"""Valeurs des attributs"""
# instructions lancées lors de la déclaration
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
# ************************
# ************************
# ** Variables globales **
# ************************
# ************************
# fenetre principale
fenetre = tk.Tk()
# Paramètres du système (variables)
IUz1 = tk.StringVar()
IUm1 = tk.StringVar()
IUz2 = tk.StringVar()
IUm2 = tk.StringVar()
IUrapport = tk.StringVar()
# ***************
# ***************
# ** 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:
roue1 = pignon(valeurZ1, valeurM1)
roue2 = pignon(valeurZ2, valeurM2)
IUrapport.set(roue1.rapport(roue2))
except:
IUrapport.set("Erreur")
# ***********************
# * Interface graphique *
# ***********************
def configureFenetre():
"""Configuration de la fenêtre principale"""
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)
# 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 **
# *************************
# *************************
configureFenetre()
# Affichage et activation de la fenêtre
fenetre.mainloop()
Annotations
modifierUne 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 :
int
—float
—str
—bool
—list
—tuple
—dict
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
- (en) « PEP 526 -- Syntax for Variable Annotations », sur Python.org (consulté le 5 avril 2019)
- (en) « PEP 3107 -- Function Annotations », sur Python.org (consulté le 5 avril 2019)
Décorateur
modifierUn décorateur 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 le décorateur.
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 :
- Déclarer le décorateur : une fonction qui s'applique à une autre fonction.
- Affecter le décorateur à la fonction visée : en mettant
@décoration
juste avant la définition de la fonction.
Par exemple :
def decorateur(f):
print("Avant la fonction")
f()
print("après la fonction")
@decorateur
def afficheFoo():
print("Foo.")
afficheFoo
# Avant la fonction
# Foo.
# Après la fonction
Lorsque l'on appelle <codeafficheFoo, on appelle en fait decorateur(afficheFoo)
.
Si la fonction à modifier admet des paramètres, il faut définir une fonction enveloppante dans le décorateur. Par exemple, nous définissons ci-dessous un décorateur 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 du décorateur 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
On peut par exemple utiliser un décorateur pour la mémoïsation. La mémoïsation est une méthode consistant à mémoriser les valeurs d'une fonction au fur et à mesure de son utilisation ; ainsi, si l'on veut évaluer la fonction avec les mêmes entrées, on se contente d'aller chercher la valeur enregistrée ce qui est plus rapide. On sacrifie donc la place mémoire au profit de la rapidité. On peut trouver des décorateurs de mémoïsation aux adresses suivantes :
- https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize
- https://gist.github.com/robcowie/1357800
- Ressources
- (en) « PEP 318 -- Decorators for Functions and Methods », sur Python.org (consulté le 5 avril 2019)
Manipulation de fichiers
modifierImporter le contenu d'un fichier
modifierPython possède la fonction open()
qui permet d'ouvrir un fichier. Ouvrir signifie qu'il crée un objet de type file
qui possède notamment les méthodes read()
et write()
. Il peut s'agir d'un objet de type « fichier binaire » (binary file) ou « fichier texte » (text file).
Si par exemple on veut utiliser (et donc lire) le contenu du fichier texte monfichier.txt
, on écrit :
fichier = open("monfichier.txt", "rt")
…
fichier.close()
Le paramètre "rt"
signifie que nous ouvrons le fichier en lecture (read) et qu'il s'agit d'un objet de type fichier texte.
Notons deux choses :
- en faisant cela, nous ne faisons qu'associer le fichier à un objet Python, nous n'avons pas encore importé les données ;
- si nous ouvrons le fichier, il faut le fermer par la suite ; c'est pourquoi nous utilisons la méthode
.close()
.
Pour éviter d'avoir à fermer le fichier, nous pouvons l'ouvrir au sein d'un contexte :
with open("monfichier.txt", "rt") as fichier:
…
Notons aussi que la chaîne de caractères indiquant le nom du fichier peut contenir le chemin d'accès au répertoire (dossier), mais sous Microsoft Windows, il faut utiliser des barres de fractions /
pour séparer les sous-répertoires au lieu de la barre inversée habituelle, par exemple :
chemin = "C:/Temp/monfichier.txt"
with open(chemin, "rt") as fichier:
…
Pour mettre les données du fichier dans la variable contenu
, nous écrivons donc :
with open("monfichier.txt", "rt") as fichier:
contenu = fichier.read()
print(contenu)
et si nous ne voulons lire que les n
premiers caractères (n
étant un entier), nous utilisons contenu = fichier.read(n)
. Cette lecture est séquentielle, c'est-à-dire que si nous appliquons la méthode plusieurs fois, nous reprenons la lecture là où nous l'avons laissée.
Si nous voulons lire une ligne, nous utilisons la méthode .readline()
. La lecture ligne par ligne est également séquentielle. Nous pouvons aussi créer une liste dont chaque élément est une ligne du fichier ; nous utilisons alors la méthode .readlines()
(notez le pluriel).
Chaque élément de la liste se termine par le caractère de fin de ligne \n
. Pour l'enlever, nous pouvons utiliser la méthode .rstrip()
pour chaque élément de la liste, par exemple. L'exemple complet est alors :
with open("monfichier.txt", "rt") as fichier:
contenu = fichier.readlines()
contenu = [item.rstrip() for item in contenu]
print(contenu)
Exporter du contenu vers un fichier
modifierSi nous voulons créer un fichier texte pour y mettre le contenu de la variable texte
, alors nous utilisons :
with open("monfichier.txt", "wt") as fichier:
contenu = fichier.write(texte)
Le module principal important pour la manipulation de fichiers est est os
.
Exploiter le contenu d'un fichier texte
modifierAvec un fichier texte, la méthode .read()
crée une variable de type texte. Nous pouvons séparer cette variable en différentes lignes avec la méthode .splitlines()
. Cela crée une liste de chaînes de caractères, chaque chaîne étant une ligne.
Si maintenant une ligne contient plusieurs données séparées par un séparateur commun, par exemple un espace, nous pouvons séparer les données par la méthode .split(séparateur)
. Cela crée une liste de chaînes de caractères, chaque chaîne étant une donnée.
Si par exemple le fichier est du type CSV (comma separated values, valeurs séparées par une virgule), l'exploitation du fichier est :
with open("monfichier.txt", "rt") as fichier:
contenu = fichier.read()
contenu = contenu.splitlines()
contenu = [item.split(",") for item in contenu]
La variable contenu
est une liste de listes. Pour avoir la ne valeurs de la me ligne, on utilise :
contenu = [m-1][n-1]
Dans certains fichiers CSV, les séparateurs de valeurs ne sont pas des virgules, on peut donc utiliser un autre caractère pour le séparateur. Si le séparateur est une tabulation, on utilise \t
: contenu = [item.split("\t") for item in contenu]
Si la première ligne contient les en-têtes des colonnes, on peut l'enlever avec la fonction del()
:
with open("monfichier.txt", "rt") as fichier:
contenu = fichier.read()
contenu = contenu.splitlines()
del(contenu[0])
contenu = [item.split(",") for item in contenu]
Certains logiciels créent des fichiers en utilisant le séparateur décimal régional, qui en France est la virgule. Pour remplacer les virgules par des points, on peut utiliser la méthode .replace()
, de préférence avant de séparer les valeurs :
contenu = contenu.splitlines()
contenu = [item.replace(",", ".") for item in contenu] # remplace les virgules par des points
contenu = [item.split(";") for item in contenu] # si le séparateur est un point-virgule
en effet, lorsque l'on a séparé les valeurs, on a une liste de liste, il faut alors balayer les sous-listes ce qui prend plus de temps :
contenu = contenu.splitlines()
contenu = [item.split(";") for item in contenu] # si le séparateur est un point-virgule
contenu = [[subitem.replace(",", ".") for subitem in item] for item in contenu] # remplace les virgules par des points
Cas d'un fichier CSV
modifierSi le fichier CSV ne contient que des valeurs numériques, on peut utiliser :
valeurs = np.loadtxt(chemin+nomfic, delimiter=",") # si le séparateur est une virgule
Il existe un module csv
dédié aux fichiers CSV. La manipulation du fichier se fait comme suit :
import csv
with open(chemin+nomfic, "rt") as fichier:
lecteur = csv.reader(fichier, delimiter=",")
contenu = [ligne for ligne in lecteur]
print(contenu)
Utilisation de Pandas
modifierPandas[4] est un module gérant les tableaux de données, appelés data frames. Voici quelques commandes utiles :
import numpy as np
import pandas as pd
M = np.random.rand(10, 10) # crée une matrice NumPy aléatoire de dimension 10 × 10
tableau = pd.DataFrame(M) # transforme la matrice en tableau DataFrame
tableau.to_csv("tableau.csv") # enregistre le tableau dans un fichier CSV
donnees = pd.read_csv("tableau.csv").to_numpy() # lit le fichier et transforme le tableau DataFrame en matrice NumPy
Exporter un programme Python
modifierVous 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- « PEP 8 -- Style Guide for Python Code », sur Python documentation (consulté le 14 mars 2019)
Notes et références
modifier- ↑ « Tabs or Spaces? », sur Python documentation (consulté le 14 mars 2019)
- ↑ « Introduction à *args et **kwargs », sur Developpez.com (consulté le 9 mars 2019).
- ↑ 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.
- ↑ https://pandas.pydata.org/