Programmation en Python, classe de Première

Ce qui suit servira essentiellement à l'enseignant. La présence d'un [PAA] dans le texte fait référence au livre PAA ci-dessous, dont la lecture peut débuter par l'enseignant tout de suite, et par l'élève en classe Terminale pour se poursuivre dans le Supérieur. L'élève en classe de Première pourra consulter le Mémento en pdf.

  PAA

Que dit le B.O. sur le programme de Première ?

Les connaissances à acquérir

Il s'agit d'une présentation par points d'intérêts, et non un découpage en temps. Dans le cadre d'un cours magistral, l'enseignant aura un ordinateur rétro-projeté pour visualiser l'interaction live avec Python. Les élèves auront une tablette ou seront en salle informatique.

1. Révisions de Seconde

Il est indispensable de faire au moins deux séances de révision, en reprenant une galerie de fonctions typiques avec ou sans résultat, illustrant les points essentiels du programme  de Seconde : affichages avec print, paramètres et arguments, variables locales et globales, conditionnelle, boucles, graphisme de la tortue. Les types étudiés en Seconde étaient int, float, bool et un peu str. Il est probable, au vu de leur expérience variable dans la pratique et l'enseignement de la programmation, que tous les enseignants n'auront pas traités dans les mêmes détails certaines parties, notamment l'affectation et les variables locales/globales. Je vous renvoie à la partie Seconde qui me semble couvrir assez bien ce qu'on attend aussi d'une Première. Les compléments spécifiques à la classe de Première (dont les listes et les modules) sont traités ci-dessous.

2. Fonctions et méthodes en Python [PAA §2.22]

Un appel de fonction se présente sous la forme f(a,b,c) : pensons simplement aux fonctions des maths. Mais Python (comme tous les langages à objets, peu importe) utilise une autre forme obj.f(a,b,c)obj est un objet Python capable de réagir à la fonction f prévue dans son type. Par exemple, pour remplacer dans une chaîne s chaque apparition de 'x' par 'y', on écrira s.replace('x','y') au lieu du traditionnel replace(s,'x','y'). On dit que replace est une méthode du type - ou classe - str : il s'agit bien d'une fonction, mais qui doit demander à un objet (ici s) d'effectuer le calcul, en lui passant des informations par ses arguments.

>>> s = '2 + 3 - 4 + 5'
>>> s.replace('+','*')  # un message envoyé à s
'2 * 3 - 4 * 5'
>>> replace             # la fonction replace n'existe pas
NameError: name 'replace' is not defined
>>> str.replace         # mais la méthode replace du type str oui !
<method 'replace' of 'str' objects>

NB. i) Je conseille à l'oral de parler de la méthode .replace en insistant sur le point obligatoire.
ii) Les classes d'objets, leurs méthodes, etc. forment la Programmation Orientée Objets (POO), dont l'étude (en particulier la construction d'une nouvelle classe) n'est pas au programme de la classe de Première.

3. Compléments sur les chaînes (str)

3.1 Unicode [PAA §3.1 et 3.2]

Dans le cadre d'un cours de maths (contrairement à ISN), ils se peut que str ne soit pertinent qu'au niveau du traitement des entiers comme suite de caractères (si l'on nomme caractère une chaîne de longueur 1). On peut éventuellement prolonger en Première l'étude de str par une introduction à Unicode axée sur les fonctions ord et chr permettant de passer d'un caractère à son numéro Unicode et inversement (ce qui permet les exos sur les codes secrets, même en se limitant à celui de César [PAA §11.42]).

A noter : Unicode contient des symboles mathématiques intéressants pour la présentation des résultats. Sur la page Web du lien précédent, vous trouvez par exemple le symbole flèche gauche utilisé pour l'affectation en "langage naturel" :

Son numéro Unicode est 2190 mais attention, il s'agit d'une écriture hexadécimale, en base 16. En Python, on écrirait 0x2190 :

 >>> 0x2190          # 2190 en base 16 n'est autre...
 8592                # ... que 8592 en décimal (base 10)
 >>> hex(8592)       # et inversement
 '0x2190'            # sous forme de chaîne hexa
 

Pour incorporer dans une chaîne un caractère Unicode dont on connaît le code hexa, il suffit de faire précéder ce dernier par \u :

>>> print('x \u2190 10')
x ← 10

Python est compatible Unicode, donc vous pouvez incorporer (si votre ordinateur et votre éditeur Python le permettent facilement) des caractères Unicode dans vos chaînes de caractères. Vous avez même le droit d'utiliser les lettres grecques Unicode dans les noms de variables :

>>> from math import pi as π
>>> π
3.141592653589793
>>> Δ = b*b - 4*a*c

NB. i) Un caractère Unicode est encodé en mémoire par 1, 2, 3 ou 4 octets. Comme les pages Web, un fichier Python a un encodage, et Python a opté pour le plus répandu UTF-8 (90% des pages Web). Son avantage est qu'il est compatible avec l'ancien code ASCII. Si vous utilisez un éditeur autre que celui livré avec Python, veillez à ce que l'encodage soit bien UTF-8.
ii) Attention, Unicode dévore le temps, surtout si les élèves voient les émoticons...

2.2 Sur la construction d'une chaîne (.format et f-chaînes) [PAA §3.4]

Pour afficher une phrase dépendant de variables, on peut utiliser une chaîne formatée en utilisant la méthode .format du type str. La chaîne doit contenir des trous {} qui vont être remplis par les valeurs des arguments de la méthode .format :

>>> (a,b) = (2,3)
>>> print('Les {} ou {} bateaux à {} €'.format(a,b,a+b))
Les 2 ou 3 bateaux à 5 €

Une autre manière plus récente et plus simple de faire (2015) consiste à utiliser les f-chaînes (f-strings) :

>>> print(f'Les {a} ou {b} bateaux à {a+b} €')     # notez le f devant la chaîne !
Les 2 ou 3 bateaux à 5 €

4. Compléments sur l'affectation

L'affectation est intimement liée à l'existence de variables, mais elle peut aussi concerner des objets composés de variables, comme les tuples (type tuple).

>>> (a,b) = (2,3)        # a = 2 et b = 3
>>> a + b
5

NB. La notation a,b au lieu de (a,b) était utilisée en Python version 2. Elle reste acceptée mais mieux vaut ne pas l'utiliser à mon avis.

>>> a, b = 2, 3          # Python 2

Nous verrons plus loin que cette affectation multiple (qualifiée de déstructurante [PAA §5.4]) s'étend aussi aux listes. Une application immédiate est l'échange de deux variables :

>>> (a,b) = (2,3)
>>> print(f'a = {a} et b = {b}')
a = 2 et b = 3
>>> (a,b) = (b,a)        # ECHANGE !
>>> print(f'a = {a} et b = {b}')
a = 3 et b = 2

So far so good. Mais ATTENTION, cet échange montre que l'affectation multiple générale (a,b) = (A,B)A et B sont deux expressions Python quelconques, n'est PAS équivalente à deux affectations simples. En général :

(a,b) = (A,B)    a = A ; b = B

>>> (a,b) = (3,2)                    |    >>> (a,b) = (3,2)
>>> (a,b) = (b,a+b)    # <--- (1)    |    >>> a = b ; b = a+b    # <--- (2)
>>> print(f'a = {a} et b = {b}')     |    >>> print(f'a = {a} et b = {b}')
a = 2 et b = 5                       |    a = 2 et b = 4

NB. En y réfléchissant un peu, l'affectation multiple (1) suit la règle usuelle : évaluation du tuple de droite dans le contexte courant, puis affectation du résultat au tuple de gauche. Alors que (2) suit l'ordre : évaluation, affectation, évaluation, affectation, et le contexte est modifié après la première affectation, perturbant ainsi la seconde évaluation.

5. La grosse nouveauté : les LISTES [PAA chap. 5]

Les listes ressemblent fort aux tuples et aux chaînes, les trois sont des séquences d'objets numérotés [PAA §5.2]. Comme les tuples et les chaînes, les listes sont numérotées de gauche à droite en partant de 0, ou de droite à gauche en partant de -1. On peut :

La particularité des listes tient au fait qu'elles sont MUTABLES : on peut modifier l'élément numéro i sur place, sans créer de nouvelle liste (ceci était impossible avec les tuples et les chaînes).

>>> L = [3, 7, -2, 1, 8]   # une liste définie en extension avec ses crochets
>>> type(L)
<class 'list'>
>>> len(L)
5
>>> (L[0], L[3], L[-3], L[-1])    # un tuple avec ses parenthèses
(3, 1, -2, 8)
>>> -2 in L                # -2 ∈ L ?
True
>>> L[2] = 0               # <--- une mutation de L, sur place
>>> L
[3, 7, 0, 1, 8]

NB. Pour se persuader que la liste L est toujours à la même place en mémoire après la mutation, demandez la valeur de id(L) avant et après [PAA pp. 86-87]. Ce dernier retourne l'adresse de L en mémoire.

5.1 L'ajout d'un élément à une liste, sur place : .append

Si L est une liste, l'expression L + [x] construit une copie de L avec un nouvel élément x rajouté à sa droite. Sans modifier L.

>>> L = [3, 7, 0, 1, 8]
>>> id(L)                    # cf NB du § 5
4479418184
>>> L1 = L + [5]
>>> L1
[3, 7, 0, 1, 8, 5]
>>> (id(L), id(L1))          # L n'a pas bougé, L1 est une copie située ailleurs...
(4479418184, 4479064136)

Idem si l'on rajoute à L un élément à gauche ou même n'importe où. En manipulant de grosses listes, cela finit par avoir un coût prohibitif. La méthode .append demande à une liste d'accueillir un nouvel élément à son extrémité droite (pas gauche !). Elle ne retourne pas de résultat, mais a un EFFET : celui de faire muter L sur place.

>>> L.append(5)              # L, ajoute-toi 5 à droite !
>>> L
[3, 7, 0, 1, 8, 5]           # sans créer de nouvelle liste

L'avantage de la mutation L.append(x) sur l'affectation L = L + [x] tient à l'efficacité : .append ne coûte qu'une unité de calcul quelle que soit la longueur de L, alors que L = L + [x] recopie L et coûte donc la longueur de L. Mais :

NB. i) Je m'aperçois qu'une section sur .append s'est évaporée de [PAA] lors de la mise en page finale : L.append(x) est utilisé pour la première fois en haut de la page 86 mais jamais expliqué. Honte sur moi ! Ce n'est qu'une méthode comme les autres sur les listes, mais largement utilisée (parfois trop).
ii) Le coût de .append est 1 unité de calcul. Le moyen pour y parvenir malgré l'encombrement de la mémoire est un peu technique. Disons que le coût est amorti sur un grand nombre d'utilisations...

5.2 Le danger des mutations de listes [PAA §5.6]

On veut bien accepter le danger, mais autant être prévenu. Tout d'abord, qu'est-ce l'affectation entre listes ?

>>> L
[3, 7, 0, 1, 8, 5]
>>> L1 = L                   # affectation et non mutation !
>>> L1
[3, 7, 0, 1, 8, 5]

L et L1 ont les mêmes éléments, donc elles sont égales au sens de ==. Mais mieux que cela : ce sont deux noms différents (des alias) pour un même objet en mémoire.

>>> id(L) == id(L1)          # ont-elles la même carte d'identité ?
True                         # donc L et L1 sont au même endroit !
>>> L1 is L                  # beaucoup plus fort que ==
True

Et là, nous sommes face à une situation qui dans certains cas devient dramatique. Je modifie un élément de L, et du coup un élément de L1 va être modifié :

>>> L[2] = 1000
>>> L
[3, 7, 1000, 1, 8, 5]
>>> L1
[3, 7, 1000, 1, 8, 5]

Normal direz-vous ? Ok, vous êtes prévenu. Montrez ça aux élèves avec le Python Tutor...

5.3 Construction ou parcours d'une liste avec une boucle

Construction de la liste des carrés des entiers de [a,b] avec a et b entiers :

def carrés(a,b):
    res = []                # on part de la liste vide [ ]
    for k in range(a,b+1):  # et de a jusqu'à b inclus...
        res.append(k*k)     # mutation : ajouter k*k à droite de res
    return res

Parcours d'une liste d'entiers pour obtenir le nombre d'entiers pairs. Notez le for n in L pour parcourir L de gauche à droite élément par élément, et for i in range(len(L)) pour parcourir les indices de L en croissant à partir de 0. Le numéro du dernier élément de L est len(L)-1.

def nbpairs(L):             | def nbpairs(L):
    res = 0                 |     res = 0
    for n in L:             |     for i in range(len(L)):
        if n % 2 == 0:      |         if L[i] % 2 == 0:
            res = res + 1   |             res = res + 1
    return res              |     return res

5.4 Construction d'une liste en compréhension

Vous avez peut-être déjà vu les sommes en compréhension en Seconde. Ici nous jouons avec les élégantes compréhensions de listes. Voici la fonction carrés ci-dessus recodée dans ce style :

def carrés(a,b):
    return [k*k for k in range(a,b+1)]   # beaucoup plus joli, non ?

La somme des carrés de [1,100] peut alors se coder ainsi :

>>> sum(k*k for k in range(1,100))
328350
>>> sum(carrés(1,100))
328350

Et la somme des carrés des impairs ajouterait une condition à la compréhension :

def carrés_impairs(a,b):
    return [k*k for k in range(a,b+1) if k%2 == 1]

NB. Comme avec les tuples et les chaînes, on peut construire une copie de la sous-liste des éléments de L de l'indice i inclus à l'indice j exclu avec l'expression L[i:j]. Une telle sous-liste se nomme une tranche de liste (en anglais slice). Ne l'introduisez en Première que si c'est vraiment sympa pour résoudre un problème. Là comme souvent ailleurs, une compréhension de liste fait l'affaire car :

L[i:j] == [L[k] for k in range(i,j)]

>>> L
[3, 7, 0, 1, 8, 5]
>>> L[2:5]
[0, 1, 8]
>>> [L[k] for k in range(2,5)]
[0, 1, 8]

5.5 Tri d'une liste

Pour mettre une liste L en ordre croissant, on peut utiliser soit la fonction Python sorted (dont le résultat est une copie triée de la liste), soit la méthode sans résultat .sort pour la faire muter sur place.

>>> L
[3, 7, 0, 1, 8, 5]
>>> Lt = sorted(L)     # une nouvelle liste, sans modifier L
>>> Lt
[0, 1, 3, 5, 7, 8]
>>> L
[3, 7, 0, 1, 8, 5]
>>> L.sort()           # aucun résultat, mais un EFFET (mutation de L sur place)
>>> L 
[0, 1, 3, 5, 7, 8]
>>> (Lt == L, Lt is L)       # cf § 5.2 pour l'opérateur is
(True, False)

NB. Il existe de nombreux algorithmes de tri didactiques pour apprendre de la belle algorithmique [PAA chap. 11 exos 73 à 79].

6. La programmation modulaire

Le qualificatif modulaire fait référence au découpage d'un programme en unités plus simples et si possible ré-utilisables [PAA exo 11.6]. Décomposer un problème en sous-problèmes, voilà le travail quotidien du scientifique qui travaille. Nous avions en Seconde découpé un programme Python situé dans un fichier xxx.py en plusieurs fonctions avec ou sans résultat et pouvant s'appeler entre elles. Chaque fonction était suivie d'un ou plusieurs tests pertinents destinés à détecter ses failles. Certains adoptent une variable globale TESTS en début de fichier, qui sera mise à True si l'on veut que les tests soient effectués, ou False sinon.

# courbes.py, 18.05.2026
TESTS = False            # exécution directe sans aucun test

def truc(x,L):
    ...

if TESTS: 
    print('truc(3,[2,6,1,8]) ==',truc(3,[2,6,1,8]))
    if truc(3,[2,6,1,8]) != 1789: print('truc --> OUPS !')

Les modules [PAA §2.6 et §6.5]

Vous avez déjà rencontrés des modules prédéfinis de Python, comme math ou random, ou encore time pour le chronomètre.

Mais le programmeur organise ses propres programmes sous la forme de modules.

Tout fichier .py est potentiellement un module. Par exemple, le fichier courbes.py est un module à partir duquel on peut importer la fonction truc.

>>> from courbes import truc
>>> truc
<function truc at 0x109a0d1e0>

Etre modulaire consiste à programmer plusieurs modules, chacun ayant un rôle spécifique (le calcul, le graphisme, etc).

Deux modules graphiques turtle et matplotlib sont détaillés dans les Mémentos, le premier dans celui de Seconde et le second dans celui de Première.

7. Et les algorithmes sur les listes ?

Avec les listes, de nombreux exos classiques sont faciles à imaginer ou à trouver, dans les livres de cours ou d'activités de maths en Première. En ce qui concerne [PAA], voici une petite sélection :

11.60 (tirages aléatoires dans une liste), 11.35+61 (dérivées, matplotlib), 11.63 à 68, 11.70 (quantificateurs et ), 11.72 (triangle de Pascal), 11.73--79 (tris), 11.80 (crible pour les nombres premiers), 11.85 (stéganographie, délicat), 11.89 (polynômes creux), 11.96 (opérations sur les ensembles).

8. HORS-PROGRAMME EN 2020 : les FONCTIONS RECURSIVES

Si vous vous en tenez au strict programme officiel, ce qui suit vous sera inutile.

La récurrence n'est - à ma connaissance - pas au programme de Première, alors que l'on aborde les techniques de démonstrations. Etant à la retraite, je peux dire sans état d'âme que je trouve étonnant (sinon triste) d'enseigner les suites définies par récurrence sans introduire le principe de récurrence pour des élèves de 17 ans (qui ont 10 ans de pratique du calcul !). Et quoi de mieux que profiter de la symbiose entre les maths et l'algorithmique pour programmer... par récurrence !

def pgcd(a,b):                 |       def pgcd(a,b):
    while b > 0:               |           if b == 0: return a
        (a,b) = (b,a%b)        |           return pgcd(b,a%b)
    return a                   |

Le style de programmation de gauche est la programmation impérative, avec son modèle de machine à états. C'est le style privilégié par Python (en tout cas par son créateur). Dans ce paradigme de programmation proche de la physique, pour prouver la correction d'un programme, il faut utiliser une logique adaptée tenant compte de la séquentialité des opérations modifiant la mémoire, dans la flèche du temps. Une telle logique existe, elle est difficile et généralement non enseignée, même dans les premières années de l'enseignement supérieur.

Le style de programmation de droite est la programmation fonctionnelle qui prétend se passer des variables, de l'affectation et des boucles, au profit de la conditionnelle, de la composition des fonctions, et du principe de récurrence. Dans ce paradigme de programmation plus proche de la pensée mathématique pure, on peut, sans nouvelle logique particulière, raisonner par récurrence sur un programme construit... par récurrence. L'absence d'affectation est compensée en interne par le simple passage des paramètres lors d'un appel de fonction. Les boucles sont réalisées théoriquement avec la même efficacité [PAA §2.23] avec un type particulier de récurrence dite terminale. Le programme de droite fonctionne exactement comme celui de gauche dans les langages (issus du langage LISP, comme Scheme ou CAML) qui optimisent la récurrence, mais ce n'est hélas pas le cas de Python. Dans [PAA], je parle donc de quasi-itération : le pgcd de droite est quasi-itératif. On peut immédiatement le transformer en le pgcd de gauche !

Comme exemple de l'intérêt pédagogique de la chose, prenons le cas de la fonction puissance x,n ↦ xn avec n entier naturel. La construction mathématique avec discussion sur la parité de n est immédiate et conduit à une dichotomie (on divise n par 2 chaque fois).

             x0 = 1
n pair   ==> xn = (x2)n//2
n impair ==> xn = (x2)n//2 * x

Ces identités mathématiques se traduisent immédiatement en une fonction Python programmée par récurrence (fonction récursive) :

def puis(x,n):
    if n == 0: return 1
    if n%2 == 0: return puis(x*x,n//2)
    return puis(x*x,n//2) * x

>>> len(str(puis(2,100000)))    # nombre de chiffres de 2100000
30103                           # time = 0.1 sec

Bon, l'algorithme est rapide, son nombre d'opérations est en log(n), la récurrence n'est pas pénalisante ici même en Python. Mais supposons que nous soyons astreints à produire un algorithme itératif, avec une boucle et en gardant l'efficacité de la dichotomie. Et bien, nous avons la chance ici de pouvoir déduire une boucle à partir de l'algorihme récursif précédent par des manipulations purement algébriques [PAA exo 11.19]. Je ne dis pas que c'est une méthode générale, mais elle fonctionne aussi pour la multiplication égyptienne par exemple. En tout cas, c'est un bel exemple de travail sur la récurrence, en maths comme en programmation.

Tout cela peut (doit) bien entendu se faire en classe Terminale, mais je pense qu'un début de raisonnement math/algo par récurrence devrait être introduit en Première, au moins en spé maths.