Remarque : Survoler les termes soulignés pour obtenir plus d'informations.
Un programme, stocké sur un disque dur, SSD, ou transféré sur internet lorsqu'on le télécharge, est assimilable à de l'information : ainsi, un programme est une donnée numérique.
Le but d'un programme est de prendre une entrée (données, son, image, mouvements
sur un écran tactile ou pad) et de produire une sortie, pour résoudre un problème
donné (opérer un patient à distance, appliquer une transformation sur une image,
divertir le joueur...).
C'est naturel : les humains ont souvent été attirés par la résolution de problèmes ;
L'Histoire retient
Un problème qui peut être résolu algorithmiquement est dit «calculable» (dans le sens : il peut être résolu par une machine de Turing).
Les machines de Turing offrent un cadre théorique qui permet de répondre (négativement) au problème de décision : «pour tout problème donné, existe-t-il un algorithme qui permet de répondre en un nombre fini d'étapes, par «oui» ou «non» à ce problème» ; on verra qu'il n'existe pas de programme qui permet de voir si un programme donné s'arrête ou boucle infiniment.
Tout langage de programmation commun (Python, Java, C, Javascript) se comporte comme
une machine de Turing (à la différence près que le ruban est fini) ; c'est même démontré
mathématiquement en utilisant les instructions de bases communes à tous ces langages.
Tout programme dans ces langages peut être codé sur une machine de Turing.
Mais dans le sens de l'Histoire, ce sont les machines de Turing, avec l'aide de concepts
inventés par
Ce qui est calculable est ce qui peut être exécuté par une machine de
Turing. Une machine de Turing est un modèle théorique imaginé par
Une machine de Turing est contituée d' :
La machine de Turing permet de «programmer» des calculs. On va montrer que la fonction
prédécesseur (pour les nombres entiers positifs) est calculable, ainsi que l'addition.
Pour cela, nous avons besoin de la représentation unaire des entiers.
En unaire, l'entier x, noté x sera représenté par (x+1) caractères
"1" côte à côte. Ainsi : 0 sera représenté en unaire par "1" et
4 sera représenté par "11111".
Pour la machine suivante, on part du mot q1"# x#", où x est un entier naturel, écrit en unaire sur le ruban (la tête de lecture, comme précisé par la notation, pointe sur le premier "#" à gauche).
état courant | symbole lu | symbole écrit | déplacement | état suivant |
---|---|---|---|---|
q1 | # | # | → | q2 |
q2 | 1 | 1 | → | q3 |
q3 | 1 | # | → | q3 |
# | # | ← | q4 | |
q4 | # | # | ← | q4 |
1 | 1 | ← | q5 | |
q5 | Arrêt |
état courant | symbole lu | symbole écrit | déplacement | état suivant |
---|---|---|---|---|
q1 | # | 0 | → | q2 |
q2 | # | 1 | → | q1 |
Remarque : On peut coder une ou plusieurs machines de Turing à l'intérieur d'une machine de Turing : on parle de machine de Turing universelle
Cf la vidéo Science4All - La machine de Turing | Intelligence Artificielle 4
Voir la vidéo CNRS - Le modèle Turing
Langage : Les problèmes peuvent être codés dans un langage formel, écrits
en utilisant un alphabet (fini) de caractères pouvant être manipulés par des machines de
Turing.
Problème de décision : C'est un problème pouvant être traité par une machine
de Turing, pour lequel on peut répondre par oui ou bien par non.
Par exemple :
Décidabilité : On dit qu'un problème est décidable lorsqu'il
est calculable et qu'une machine de Turing permet de répondre "oui" par un état
final ou bien "non" par un arrêt sur un autre état, c'est-à-dire répondre au problème
en un nombre fini d'étapes. Concrètement, il existe un algorithme qui permet de répondre au problème et il s'arrête
en donnant la réponse.
Dans le cas contraire, on dit que le problème est indécidable.
S'il existe un algorithme pouvant répondant "oui" en un nombre fini d'étapes, mais ne pouvant
pas répondre "non" en un nombre fini d´étapes, on dit que le problème est semi-décidable.
Problème de l'arrêt : Ce problème cherche à savoir s'il existe un programme
(appelons-le halt()
), qui prend en entrée n'importe quel programme x (donné
sous forme de chaîne de caractères, ou bien codé sous forme de nombre, par exemple)
et précise si x s'arrête ou non.
Trop beau pour être vrai ! On va voir que ce problème est indécidable, et qu'un tel programme
halt
ne peut exister.
Examinons l'algorithme suivant :
TURING(n) # n est un entier naturel
1. LISTER (sans les exécuter) tous les programmes de taille inférieure ou égale à n caractères,
qui prennent un entier en entrée et renvoient un entier en sortie.
# c'est long, mais possible par reconnaissance de la syntaxe et de la grammaire
# de ces programmes ; aussi, ces programmes sont en nombre fini.
2. SÉLECTIONNER parmi cette liste ceux qui s'arrêtent avec halt() lorsque leur entrée est n.
3. EXÉCUTER chacun des programmes sélectionnés et choisir le plus grand des entiers Smax
apparu en sortie de ces programmes.
4. RETOURNER Smax + 1.
Une fois cet algorithme programmé, notons n0 sa taille en caractères.TURING()
s'arrête.TURING(n0)
:
TURING(n0)
se liste lui-même puisque sa taille est inférieure
ou égale à n0. halt
.TURING(n0)
renvoie un entier qui est forcément inférieur ou égal à Smax...
halt
ne peut donc pas exister : sinon, on pourrait s'en servir pour
écrire un programme qui renvoie une sortie plus grande que la sortie qu'il renvoie.
Pour exécuter un programme, on peut, encore aujourd'hui, produire un exécutable à partir
d'un script où sont écrites, à l'aide d'une syntaxe minimale, une suite d'
instructions (hardware) du processeur permettant d'agir directement sur les registres.
On appelle ce pseudo-langage l'assembleur ; il est propre à
chaque processeur car leurs
jeux d'instructions peuvent être différents. On le qualifie de langage de bas niveau, dans
le sens où il est très proche du matériel ; le plus proche étant le langage
machine, qui
est essentiellement de l'assembleur écrit en binaire. Voici un exemple d'opération écrite
en assembleur (ces instructions sont assez communes et ce script marcherait sur bon
nombre de processeurs) :
;pour calculer (7+6)%3, on aura le code suivant :
MOV AX, 7 ; Le registre AX, reçoit la valeur 7.
ADD AX, 6 ; On ajoute à AX la valeur 6, le résultat est stocké dans la destination, soit AX.
MOV BX, 3 ; On met dans le registre BX la valeur 3 qui servira de source à la division.
DIV BX ; On divise AX par la source, ici BX, qui vaut 3
;le résultat de la division est stocké dans la destination (BX)
;et le modulo (reste de la division qui nous intéresse ici) est mis dans DX
Cf Wikiversity
Ce type de travail est amusant au départ, mais vite fastidieux si l'on veut développer des
programmes un peu longs. C'est une enseignante en mathématiques, devenue ingénieure informatique dans la navy,
![]() Le rôle d'un compilateur est de transformer un code source, écrit dans un langage (plus naturel que l'assembleur) en un fichier binaire exécutable sur une architecture matérielle et un système déxploitation donné. |
![]() Il s'oppose à l'interpréteur, qui est un logiciel qui exécute directement le code source d'un langage (interprété). |
Niveau de langage :
|
![]() |
Le terme paradigme signifie à la fois :
main = do
putStrLn "Hello World !"
fac n = product [1..n]
fibs = 0 : 1 : (zipWith (+) fibs (tail fibs))
fib n = fibs !! n
Attention, l'idée du paradigme fonctionnel n'est pas seulement d'utiliser des fonctions mais d'utiliser des fonctions dites pures. Il s'agit de fonctions qui ne modifient pas l'état courant des variables. Autrement dit, le résultat renvoyé par une fonction pure ne doit dépendre que de valeurs passées en paramètres et pas de valeurs externes à la fonction. De plus une fonction pure ne doit pas modifier les valeurs des variables externes, globales.
def est_majeur(age):
return age >= 18
age
.
De plus on constate qu'elle ne modifie aucune variable externe.
liste = [1, 6, 4]
def ajouter(x):
liste.append(x)
return liste
liste
def ajouter(liste, x): # Cette fonction est pure
liste = liste + [x]
return liste
Attention, la fonction suivante n'est pas pure !
def ajouter(liste, x): # Cette fonction n'est pas pure
liste.append(x)
return liste
En effet, en python, la méthode append
va modifier la variable externe liste
qui a été passée en paramètre de la fonction.
Tandis que la première fonction créé une réelle copie de la variable liste
au sein de son espace de variables, grâce à l'opérateur de concaténation,
la deuxième fonction agit directement sur la variable externe liste
ce qui correspond à un effet de bord.
Avec un même langage de programmation, on peut utiliser des paradigmes différents.
Dans un même programme, on peut utiliser des paradigmes différents.
# Récupère la liste de nombres
liste = input("Entrez la liste de nombres séparés par un espace : \n").replace(",", ".").split()
somme = 0
for nb in liste: # boucle sur la liste
somme = somme + float(nb)
print(somme) # affiche la somme
def somme_liste(liste): # définit la fonction (procédure) pour effectuer le calcul
somme = 0
for nb in liste:
somme = somme + float(nb)
return somme
# Récupère la liste de nombres
liste = input("Entrez la liste de nombres séparés par un espace : \n").replace(",", ".").split()
s = somme_liste(liste) # récupère la somme
print(s) # affiche la somme
def somme_liste(liste): # définit une fonction pure récursive pour effectuer le calcul
if len(liste) == 0: # cas trivial
return 0
return float(liste[0]) + somme_liste(liste[1:]) # appel récursif pour effectuer la somme
# Récupère la liste de nombre, appele la somme et affiche le résultat.
print(somme_liste(input("Entrez la liste de nombre : \n").replace(",", ".").split()))
class Calculateur: # Constructeur d'objet
def __init__(self, liste): # Initialisation de l'objet
self.liste = liste
def somme(self): # méthode de calcul de la somme à partir de sa liste
somme = 0
for nb in self.liste:
somme = somme + float(nb)
return somme
unCalcultateur = Calculateur(input("Entrez la liste de nombre : \n").replace(",", ".").split()) # Invocation d'un objet Calculateur
print(unCalcultateur.somme()) # Appel de la méthode somme() et affichage du résultat
Cette partie est la suite logique du chapitre sur les spécifications et les tests vu en première NSI.
import this
dans la console python pour re-découvrir les
grands principes de python : The Zen of Python (PEP 20)Rappel de la syntaxe python des assertions (1re) :
assert condition
assert condition, "message d'erreur"
L'assertion vérifie qu'une condition est respectée, sinon elle stoppe l'exécution du programme et
peut afficher un message d'erreur (celui-ci est facultatif).
Une assertion permet de stopper préventivement une exécution avant qu'une erreur ne se produise. Elles
sont des outils de développement et ne doivent pas apparaître dans un programme final, livrable. En supprimant toutes les
assertions d'un script, le programme doit continuer à fonctionner normalement.
Le mode de programmation qui utilise des assertions est appelée programmation défensive. Elle suppose que les fonctions sont correctement utilisées,
mais prévoit des garde-fous : les assertions, qui permettent de vérifier les préconditions et les postconditions et ainsi de détecter des bugs
au cours du développement.
Ce type de programmation est utilisé pour du code dont on a le contrôle. Par exemple lorsqu'on écrit un module, pour les fonctions qui sont destinées à n'être appelée que dans ce module. Pour les fonctions destinées à être appelées par d'autres modules/scripts/personnes, il est préférable de faire une gestion active des erreurs avec une structure conditionnelle qui retourne des valeurs facilement interprétables et si possible de même type que autres retours.
def indice(liste, element):
"""Renvoie l'indice de la première occurence de l'élément dans la liste."""
assert element in liste, "L'élément doit être dans la liste."
return liste.index(element)
print(indice(["a", "b", "c", "d"], "e"))
def indice(liste, element):
"""Renvoie l'indice de la première occurence de l'élément dans la liste. Renvoie -1 si l'élément n'est pas dans la liste."""
if element not in liste:
return -1
return liste.index(element)
Ce comportement, qui renvoie -1 quand l'élément n'est pas dans la liste, est celui de la méthode liste.indexOf(element)
en javascript.
La modularité est le fait de décomposer un programme en plusieurs fonctions/modules/fichiers.
La modularité présente des avantages d'autant plus fort qu'un projet est important :
Cette modularité existe à plusieurs échelles :
Un module constitue une "brique" logicielle d'un projet, pouvant éventuellement être utilisé par d'autres projets. Il est donc important de s'assurer de son indépendance vis-à-vis du reste du projet et de la cohérence des missions qu'il assure. De plus, il est important de le documenter convenablement afin de le rendre compréhensible et facilement réutilisable.
Il existe un grand nombre de modules natifs au moteur Python. La liste exhaustive est disponible dans la Doc Python en ligne. Parmi eux, en voici quelques-uns particulièrement utilisés :
Il en existe beaucoup d'autres, développés par la grande communauté des développeurs Python et qui nécessiteront une installation supplémentaire. Ils sont en général accompagnés d'un site internet les documentant :
En python, les modules sont importés grâce au mot clé import
proposant plusieurs syntaxes :
Syntaxe 1 :
|
Syntaxe 2 :
|
Syntaxe 3 :
|
Syntaxe 4 :
|
Attention, la première syntaxe importe directement dans l'espace des noms du programme, tout ce que contient le module (*).
Cela peut entraîner une surcharge de nom, c'est-à-dire l'écrasement d'une variable ou d'une fonction par une autre varible ou une autre fonction portant le même nom.
Ce risque de surcharge est facilement évitable à l'aide des autres syntaxes. En n'important qu'une fonction du module (syntaxe 2)
ou en nommant explicitement le module (syntaxe 3) ou en le renommant (syntaxe 4) lors de l'utilisation d'une de ses fonctions.
Pour créer vos propres modules, il suffit de créer un fichier python mon_module.py
à la racine de votre projet et de
l'importer avec les mêmes syntaxes proposées par l'instruction import
.
from mon_module import *
from mon_module import ma_fonction
import mon_module
import mon_module as mm
Dans le cas d'un site internet, la modularité est déjà prise en compte dans le découpage fonctionnel des fichiers HTML, CSS et JS. Pour rappel, les fichiers HTML hébergeant le contenu du site, les fichiers CSS, sa mise en page et les fichiers JS assurant son interactivité.
Comme pour la plupart des langages de programmation, la modularité est également permise en javascript.
Par exemple sur ce site de cours de NSI, la mise en page des codes est sous-traitée à un module
(fichier javascript) intitulé prism.js
utilisant également un fichier CSS prism.css
. Ces deux
fichiers ont été téléchargés depuis le site prismjs.com et intégrés
aux fichiers du site.
De la même façon qu'en python, les modules sont importés en début de script, nous trouverons dans la balise
<head>
d'une page HTML, des liens vers des fichiers javascript. Ces fichiers peuvent tout aussi bien être locaux (repéré par leur chemin relatif) ou bien hébergé
ailleurs sur internet (repéré par leur URL).
<head>
<meta charset="utf-8" />
<title>C9-Programmation</title>
<link rel="stylesheet" type="text/css" href="styles/style.css" />
<link rel="stylesheet" type="text/css" media="print" href="styles/print.css"/>
<link href="styles/prism.css" rel="stylesheet" />
<script src="scripts/interactif.js" defer></script>
<script src="scripts/prism.js" defer></script>
<script async="true" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=AM_CHTML" defer></script>
</head>
MathJax.js
hébergé sur https://cdn.jsdelivr.net
.
Son rôle est d'assurer l'affichage correct d'équations mathématiques écrites en AsciiMath dans le code source de la page. Le texte AsciiMath doit être encadré par des accents graves.
x = y^2 + sqrt(y)/3
s'affichera : `x = y^2 + sqrt(y)/3`. La syntaxe de l'AsciiMath est détaillée sur http://asciimath.org/.
Le terme "bug" a été inventé par Grace Hopper en référence à un insecte coincé dans un ordinateur en 1974 et qui avait généré un problème.
En programmation, savoir répondre aux causes de bugs et tester ses scripts est essentiel pour obtenir un programme fiable et robuste.
Un bon Environnement de Développement Intégré (EDI) fournit plusieurs outils permettant de limiter l'apparition de bugs. Il doit permettre :
Conçu pour le langage python, la version PyCharm Community
est un EDI libre, gratuit et assez ergonomique qui propose ces fonctionnalités.
Vous pouvez les retrouver également dans Visual Studio Code qui est un
EDI généraliste (plusieurs langages) et qui propose de nombreuses extensions très intéressantes.
while
:
for
, il faudra éviter pendant l'exécution de la boucle de modifier la liste que l'on est en train de parcourir.
a = 0.0
while a != 0.3:
a += 0.1
print(a)
print("C'est fini !")
0.3
.
Le problème vient de l'approximation des flottants et ici de la comparaison dans la boucle while
.
Ici la correction consiste à s'assurer que le variant de boucle (ici a) bascule définitivement la condition comme fausse à l'aide d'un strictement inférieur.
|
Une autre solution si l'on a vraiment besoin de vérifier l'égalité de flottants est d'utiliser un encadrement de la valeur dans la condition.
|
def supprimer_les_nombres_pairs(liste):
for i in range(len(liste)):
if liste[i]%2 == 0:
liste.pop(i)
return liste
liste = [i for i in range(10)]
print(supprimer_les_nombres_pairs(liste))
IndexError: list index out of range
. En effet, la liste est modifiée au cours des itérations
de la boucle or vient un moment où l'indice demandé ne renvoie plus rien car la liste est plus courte qu'au départ de la boucle.
Une possibilité est de créer une seconde liste que l'on rempli avec seulement les éléments retenus.
|
Attention mauvaise solution : Ici la fonction enumerate(liste) ajoute un compteur
à la liste permettant d'accéder à des tuples (indice, valeur). Ce compteur s'adapte à la taille de la liste pour empêcher l'erreur
IndexError .
Bien que dans certains cas liste = [0, 1, 2, 3] , la réponse soit correcte,
cette solution est totalement fausse. En effet, la liste est toujours en train d'être modifiée au cours des itérations et si elle répond bien
ce n'est pas quelle a gardé les nombres impairs mais qu'elle les a sauté dans son parcours de la liste du fait que celle-ci est racourcie au fur et à mesure.
Vérifier que ce script donne une mauvaise réponse pour liste = [0, 2, 1, 3] .
|
Celle-ci reprend l'idée de créer une nouvelle liste et de la remplir avec les éléments retenus mais en utilisant une construction par compréhension.
|
Dans cette solution, il faut appliquer la fonction list() à l'objet renvoyé par la fonction
filter() afin de le convertir en liste.
|
Quand le moteur Python rencontre une erreur, il lève une exception.
Une exception est un objet en python qui est définie par son type.
Voici une liste non exhaustive de types d'exception :
Type d'erreur | Description |
---|---|
SyntaxError |
Le code ne peut pas être exécuté car l'analyseur a repéré une erreur dans la syntaxe de python. Il peut s'agir
de deux-point ":" manquant, de parenthèses mal couplées ou d'indentation manquante. L'analyseur qui ne reconnaît plus la syntaxe de python, signale une ligne d'erreur mais attention la faute de syntaxe est souvent localisée avant cette ligne. |
ZeroDivisionError |
Erreur de division par zéro. |
NameError |
Une variable, une fonction, une classe, invoquée ... n'est pas précédement définie. |
TypeError |
L'opérateur n'accepte pas le type d'opérande qui lui est donné. |
ValueError |
Ici l'opérateur reçoit le bon type d'opérande mais sa valeur est inappropriée. |
IOError |
Le fichier n'existe pas. |
RecursionError |
La limite de la pile d'appels récursifs a été atteinte. Elle est de 1000 par défaut et peut être modifiée. |
IndexError |
L'indice pour une séquence, ou la clé pour un dictionnaire n'existe pas. |
AssertionError |
Une assertion est fausse. |
KeyboardInterrupt |
L'exécution est stoppée par l'utilisateur (fermeture de fenêtre par exemple). Contrairement aux autres erreurs,
il ne s'agit pas d'un objet Exception , en python. Par exemple,
il ne sera pas capter par l'instruction except Exception : , contrairement à toutes les autres.
Par contre, il sera capter par l'instruction except : .
|
x = int(input("Entrez un nombre : "))
ValueError
car la fonction int()
est incapable de convertir une chaîne de caractère qui contiendrait autre chose que des entiers.
age = input("Entrez votre age : ")
if age >= 18:
print("Vous êtes majeur.")
TypeError
car l'opérateur >=
ne sait pas comparer une chaîne de
caractères avec un entier.
Python propose une syntaxe afin de gérer les erreurs.
try:
# instructions à exécuter
except """type d'erreur 1""" :
# instructions à exécuter si ce type d'erreur est levée
except """type d'erreur 2""" : # Un deuxième except est facultatif
# instructions à exécuter si ce type d'erreur est levée
else: # Factultatif
# instructions à exécuter s'il n'y a pas eu d'erreur
finally: # Facultatif
# instructions à exécuter dans tous les cas même si un return existe dans un bloc except
Remarque : Il est possible de ne pas préciser le type d'erreur à attraper
après except:
, mais cela est déconseillé
car toutes les erreurs levées ne se valent pas et cela peut être source d'erreurs supplémentaires
en capturant une erreur qui n'aurait pas dû l'être et qui sera alors invisible.
try: except: else:
afin qu'aucune erreur ne soit levée quelque soit l'action de l'utilisateur et qu'il soit informé de son éventuelle erreur.
age = int(input("Entrez votre age : "))
if age >= 18:
print("Vous êtes majeur.")
else:
print("Vous êtes mineur.")
try :
age = input("Entrez votre age : ")
except KeyboardInterrupt:
pass # Instruction vide : rien ne s'exécute
else:
try:
age = int(age)
except ValueError:
print("Vous devez entrer que des caractères numériques.")
else:
if age >= 18:
print("Vous êtes majeur.")
else:
print("Vous êtes mineur.")
Il est également possible de lever soi-même une erreur à l'aide du mot clé raise
ou
bien encore de récupérer une erreur dans une variable.
raise NameError("Ce nom de variable n'existe pas.")
.NameError: Ce nom de variable n'existe pas.
def majeur(age):
if type(age) != type(1):
raise # à compléter
if age < 0:
raise # à compléter
if age >= 18:
return "majeur"
return "mineur"
age = -1
try:
print("Vous êtes {0}.".format(majeur(age)))
except TypeError as er:
print("Erreur :", er)
except ValueError as er:
print("Erreur :", er)
def majeur(age):
if type(age) != type(1):
raise TypeError("L'age doit être de type entier.")
if age < 0:
raise ValueError("L'âge doit être un entier positif.")
if age >= 18:
return "majeur"
return "mineur"
age = -1
try:
print("Vous êtes {0}.".format(majeur(age)))
except TypeError as er:
print("Erreur :", er)
except ValueError as er:
print("Erreur :", er)
L'utilisation des assertions dans une structure de gestion des erreurs permet de personnaliser encore davantage les messages d'erreur.
age = input("Entrez votre âge :")
try:
age = int(age)
assert age >= 0, "Vous ne pouvez pas avoir un âge négatif."
assert age < 200, "Vous ne pouvez pas avoir plus de 199 ans."
except ValueError:
print("Vous devez entrer uniquement des caractères numériques.")
except AssertionError as msg :
print(msg)
En apparence, python ne se soucie pas des types de variable : lorsqu'on initialise une variable, à aucun moment on ne doit déclarer son type et à tout moment, on peut écraser le contenu d'une variable par une valeur d'un autre type. C'est assez pratique car cela allège le code mais au risque de générer des erreurs.
>>> a = "Hello, World"
>>> print(type(a))
<class str>
>>> a = 3 + 2
>>> print(type(a))
<class int>
Cet exemple montre que Python connaît bien le type de ses variables et qu'il vous autorise à en changer comme bon vous semble. Le risque est d'affecter de mauvaises valeurs à une variable sans s'en rendre compte et ainsi génerer des bugs. Certains langages comme le C oblige à déclarer au préalable les variables et leurs types. Le compilateur va alors détecter les erreurs de typage et avertir le développeur des problèmes avant même qu'ils ne se produisent mais ce n'est pas le cas en python.
Il est possible de réaliser à partir de python 3.10 des annotations de type, qui sont utiles pour le développeur. L'Environnement de Développement Intégré pourra les exploiter pour avertir en cas d'erreurs ou faire des suggestions pertinentes limitant le risque d'erreurs.
Exemple : Les deux fonctions écrites ci-dessus dans PyCharm sont fonctionnellement équivalentes mais la seconde comporte des informations de type utilisé par l'EDI pour aider le développeur, comme illustré ci-dessous.
![]() |
![]() |
Le contrôle de versions permet de suivre les modifications, faites par qui et pourquoi, ce qui devient crucial pour de gros projets et le travail d'équipe.
Voir le cours d'OpenClassrooms
import this
dans la console python pour y accéder.Bibliographie :