Programmation en Python, classe de Seconde

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 Seconde pourra consulter le Mémento en pdf.

  PAA

Que dit le B.O. sur le programme de Seconde ?

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. Les nombres (int, bool) et leurs opérations [PAA §2.2]

Parmi les objets manipulés par Python en Seconde se trouvent les nombres entiers (ou exacts) et les nombres flottants (ou approchés, inexacts, avec un point décimal). La précision maximale d'un flottant est d'une quinzaine de chiffres décimaux. Il y a donc deux types de nombres :

>>> type(-345)              # -345 est de type (ou classe) int, idem pour 3 et 345
<class 'int'>
>>> type(3.1415)            # 3.1415 est de type (ou classe) float, idem pour 3.0
<class 'float'>
>>> 234.78967865653234535657879667465       # précision limitée
234.78967865653235

NB. J'ai parlé d'objets ci-dessus, sans parler de "programmation par objets" ni simplement justifier le mot, qui est pris dans son sens tout-à-fait commun, et ça tombe bien :-)

1.1 Les nombres entiers (int, exacts) [PAA §2.3]

Les opérateurs arithmétiques sont + - * ** // %. Les trois premiers sont bien connus, et le quatrième est l'opérateur puissance x**n pour xn. Les deux derniers sont le quotient entier a // b (qui se lit a double slash b), et le reste entier a % b (qui se lit a modulo b) d'une division de l'entier a par l'entier non nul b (limitons-nous aux entiers positifs). Toutes ces opérations sur des entiers exacts produisent des entiers exacts.

>>> 12345 % 10         # le chiffre des unités
5
>>> 12345 // 10        # le nombre privé de son chiffre des unités
1234

Le PGCD se note gcd, et le ppcm lcm d'après le cours d'anglais.

NB. i) Le nombre 45 est un entier (type int) mais le nombre 45.0 n'est pas un entier, c'est un flottant (type float, on peut parler d'entier inexact)
ii) Pour tirer un entier au hasard, importez la fonction randint qui se trouve dans le module random, une bibliothèque de fonctions supplémentaires Python sur les tirages aléatoires livrée avec le logiciel Python :

>>> from random import randint        # [PAA §2.15]
>>> randint(1000,2000)
1789

1.2 Les nombres flottants (float, approchés) [PAA §2.4]

On ne parlera jamais de nombre réel mais de nombre flottant ou approché ou inexact. Les réels des mathématiques sont l'objet d'une tout autre histoire. Les nombres flottants de Python ne représentent même pas les nombres rationnels, seulement une partie des nombres décimaux du collège.

Les opérations usuelles sont encore + - * mais la division flottante (à virgule) se note simplement / (slash).

>>> 7 / 3               # calcul approché
2.3333333333333335
>>> 7 // 3              # calcul exact
2

NB. i) N'utilisez pas les opérateurs // et % avec des nombres flottants, ça produit de la confusion.
ii) On passe d'un flottant x à un entier avec int(x) qui gomme la partie décimale (ce n'est donc pas exactement la partie entière dans les négatifs). Inversement, à partir d'un entier n, on peut produire le flottant float(n) mais c'est en général inutile car un entier peut être mis là où on attend un flottant, il sera automatiquement converti en entier inexact.

On vérifie que les priorités des opérateurs sont connues en maths, et on précise les règles existant en programmation (de gauche à droite en cas de même priorité) [PAA §2.10]. On s'entraînera au parenthésage complet d'une expression arithmétique (dans quel ordre agissent les divers opérateurs ?).

Ne vous aventurez pas dans les détails de l'arithmétique 64 bits, qui sont pénibles et sources d'irritations à ce niveau d'enseignement. Il suffit de dire que les ordinateurs travaillent en binaire [PAA exo 11.2] et que certains nombres flottants sont simples en base 10 comme 0.1 par exemple, mais ont une infinité de chiffres binaires [PAA exo 11.3] et seront tronqués, d'où un manque de précision et de grosses surprises classiques [PAA §2.8] comme 0.1 + 0.1 + 0.1 qui n'est pas égal à 0.3 en Python (de manière générale, l'égalité de deux objets Python se note ==).

Les autres fonctions de l'Analyse mathématique, à part la valeur absolue abs(x) qui est dans le noyau Python, se trouvent dans le module math [PAA §2.6]. C'est le cas de sqrt, cos, etc. et de la constante pi.

>>> from math import sqrt   # importation de la fonction racine carrée
>>> sqrt(2)
1.4142135623730951          # une quinzaine de chiffres après le point décimal
>>> sqrt(2)**2
2.0000000000000004          # vous avez dit "calcul approché" ?...

NB. i) On évitera d'encourager la forme from math import * qui n'explicite pas ce dont on a besoin. Sauf pour le module turtle.
ii) Pour tirer un flottant au hasard, importez la fonction uniform du module random [PAA §2.15] :

>>> from random import uniform
>>> uniform(0,1)            # un flottant au hasard entre 0 et 1
0.17707415872912313

1.3 Opérateurs de comparaison numérique [PAA §2.13]

Tous les nombres sont donc passibles d'opérations arithmétiques, mais aussi de comparaisons < > <= >= ==. Un opérateur de comparaison retourne une valeur booléenne vrai ou faux, en Python True ou False de type bool (cf §3).

>>> 56 * 32 > 1789
True
>>> 6**5 > 5**6
False
>>> type(3 < 2)
<class 'bool'>

En vertu de ce qui a été dit un peu plus haut, ON N'UTILISERA PAS L'OPERATEUR == AVEC DES NOMBRES FLOTTANTS.

>>> 3 == 3.0
True                 # égaux alors que 3 (int) et 3.0 (float) ne sont pas de même type ?

NB. La différence entre deux objets Python se note not a == b ou mieux a != b.

2. Notion de fonction en maths et en Python [PAA §2.16]

Le discours en maths a été fait. En programmation, une fonction est un objet capable de recevoir des données (ses arguments) et de les utiliser :
- soit pour retourner un (seul) RESULTAT avec le mot-clé return.
- soit pour effectuer une action (modifier quelque chose, afficher du texte sur l'écran, etc). On dira que la fonction a eu un EFFET. Dans ce cas, elle ne devrait pas avoir de résultat.

def f(x):                        # exécuter en live chacune de ces fonctions en Python
    return x**3 -2x + 1

def g(x,y):
    return 3*x + 2*f(y)          # une fonction qui fait appel à une autre fonction

def aff_carré(x):                # fonction sans résultat mais avec un EFFET (affichage)
    print('Le carré de x pour x =',x,'est',x*x)   # expliquer le rôle de print [PAA §2.12-2.19]

NB. Il n'y a pas de parenthèse après le mot return qui n'est pas une fonction, contrairement à ce que l'on voit dans beaucoup de livres qui devaient utiliser Java auparavant et ont été traduits en Python à la va vite [PAA chap 10]. Mais vous avez le droit d'en mettre une, ou même 8 si vous voulez : c'est laid, return est un mot-clé (comme def, if ou while par exemple).

Les domaines de définition ou d'utilisation ne sont pas précisés, Python est laxiste. On peut les préciser en commentaire sur la droite du def, ou en docstring [PAA §2.21] :

def f(x):                        # On suppose que x est un nombre > 5
    return x**3 -2x + 1

def f(x):
    '''On suppose que x est un nombre > 5'''
    return x**3 -2x + 1

NB. MECANIQUE. Comment ça marche, une fonction f ? Je l'ai définie avec un def f(x,y):... et je vais l'exécuter en appelant le calcul de f(3,5+4). Les arguments 3 et 5+4 vont être évalués pour produire les valeurs 3 et 9. Ensuite, les paramètres x et y vont prendre pour valeurs respectives 3 et 9. Enfin, le texte de la fonction va être exécuté de haut en bas, produisant ou non un résultat. A la fin, on oublie les liaisons temporaires de x à 3 et de y à 9. Ouf. Compliqué ? Un peu, d'autant que ce modèle est incomplet... Mais l'idée est là. Retenir que les arguments sont tous calculés (de gauche à droite) avant que la fonction n'agisse !

Les fonctions anonymes [PAA §2.20]

Le matheux parle de la fonction (x,y) ↦ 2x-y sans lui donner un nom, ce que Python (à la suite de LISP/Scheme) notera lambda x,y: 2*x-y. Cette construction fournit des fonctions anonymes, qui sont bien pratiques pour définir vite de petites fonctions réduites à une formule, ou les passer à la volée en argument à une autre fonction (ou des choses plus intellectuelles dans les années suivantes) :

def compose(f,g):               # fonction x fonction --> fonction
    return lambda x: f(g(x))    # on retourne une fonction pure !

k = compose(abs, lambda x: x*x-1)
print('k(-3) =',k(-3))

On peut briser l'anonymat et donner un nom à une lambda-expression par une simple affectation.

       f = lambda x,y: 2*x-y      <==>       def f(x,y):
                                                 return 2*x-y

3. Les booléens (bool) [PAA §2.13]

Nous avons rencontré au §1.3 les constantes booléennes True et False. On veillera à n'utiliser que ces deux valeurs dans les tests, bien que Python soit ici aussi laxiste, et considère que True == 1 et False == 0 au sein d'une opération arithmétique (pour les électroniciens et leur logique numérique) :

>>> 5 + 3*False - True               # beurk
4

NB. Pour savoir si une valeur Python quelconque v sera considérée comme vraie ou fausse dans un test par Python, il suffit de la transformer en True ou False avec bool(v).

Les opérateurs and, or et not permettent de construire des expressions booléennes.

>>> x = 24                           # x prend la valeur 24
>>> (x % 2 == 0) or (x < 1/0)        # court-circuit, 1/0 n'est pas calculé !
True
>>> not (x >= 0)                     # <==> x < 0
False

Les opérateurs and et or sont court-circuités : à quoi bon, pour un calculateur, évaluer B dans une expression A or B si A est vrai ? Idem pour and si A est faux. En particulier and et or ne sont pas commutatifs car évalués de manière séquentielle de gauche à droite avec échappement dès que possible. J'essaye de programmer une fonction And pour remplacer l'opérateur and :

def And(x,y):                        # bool x bool --> bool
    if x == False: return False      # ce code semble court-circuité...
    return y                         # ... mais non !

La définition ci-dessus sera immédiatement incorrecte car non court-circuitée. Dans And(A,B), les arguments A et B seront évalués tous les deux avant que le mot-clé if n'agisse...

NB. Dans les algorithmes, une erreur fréquente se rencontre dans des tests écrits à l'envers du genre :
           if (1/i < 3) and (i != 0): ...

L'étape suivante est la prise de décision.

4. La conditionnelle if [PAA §2.14]

On augmente le vocabulaire des fonctions avec le mot-clé if augmenté de elif et de else avec des exemples (fonctions en escalier, affines par morceaux, valeur absolue, calcul des impôts par tranche, etc). Du calcul fonctionnel pur, pas besoin d'affectation. Seul l'opérateur == est utilisé, pas de = qui rentre trop tôt en conflit avec les maths...

Montrez le caractère optionnel du else après un return, et insistez sur le fait que return provoque un échappement immédiat :

def k(x,y):      # x et y sont des entiers naturels
    if x < y: return x        # return empêche d'exécuter les lignes suivantes
    return y     # else est donc ici sous-entendu, mais le mettre en cas de peur...

Si l'on omet le else, il faut prononcer quand même le mot "sinon" pour que ce soit bien clair. Ensuite, l'habitude viendra par mimétisme.

NB. Personnellement, j'aime bien l'expression x if condition else y qui retourne un résultat au lieu d'être seulement une instruction à effet :

def ma_valeur_absolue(x):
    return x if x >= 0 else -x       # plus court et plus proche du langage naturel

NB. Il n'y a toujours pas de variable globale hormis les fonctions. Si exceptionnellement vous avez besoin de poser c = 300000 pour faire un exo, allez-y, mais en disant bien qu'il s'agit d'une constante utilisable par les fonctions (à condition qu'un paramètre ne se nomme pas c)...

5. Les constantes locales [PAA §2.16]

On introduit les variables locales dans une fonction pour éviter les recalculs, ou simplement nommer des calculs intermédiaires. A ce niveau, ce sont plutôt des CONSTANTES locales. Un changement de variable en maths ?...

def f(x):
    x3 = x*x*x                 # opérateur = : nommons x3 le cube de x
    if x3 > 1: return x3
    return x3 / (1 + x3)       # pour ne pas calculer x*x*x deux ou trois fois

Insister sur le fait que x3 n'existe pas en-dehors du texte de f, d'où le qualificatif local.

6. La boucle bornée for [PAA §2.25]

Bornée car le nombre de tours de boucle est limité, on ne peut pas boucler à l'infini. La syntaxe courante en Seconde est :

        for x in range(a,b):      # pour chaque entier x de a inclu jusqu'à b exclu
            ...

mais où a et b sont ENTIERS. On commence par des boucles procédant uniquement à des affichages. Vous pouvez montrer comment on peut cependant avancer dans un intervalle par pas de 0.1 avec range :

def tableau(f,a,b):
    '''Affiche de 0.1 en 0.1 les points x,y de la courbe de f entre a et b'''
    for x in range(10*a,10*b):
        print(x/10,f(x/10))

def g(x):                    # ou f(x), peu importe
    return 2*x-1

tableau(g,0,1)   # visualisation d'un tableau de points

NB. i) Ci-dessus, j'ai passé une fonction g en argument. N'en faites pas toute une salade. C'est parfois délicat dans d'autres langages de programmation, mais c'est naturel en Python. Insistez sur la nature abstraite du nom f du paramètre : "ça fonctionne pour n'importe quelle fonction f". Trop pratique pour s'en passer !
ii) L'élève qui aime faire exploser la machine peut s'étonner du résultat de range(1,1000000000000). C'est que range ne contient que potentiellement ses éléments, il est seulement chargé de les générer un par un, dans une boucle for, ce qui le classe dans la catégorie des objets itérables.

6.1 Le mécanisme d'affectation et les variables locales [PAA §2.16]

Il n'y a pas que des constantes locales, il faut pouvoir disposer de VARIABLES locales, notamment lorsqu'on construit une solution pas à pas. On présente quelques exemples simples avec un accumulateur, du style :

def somme_entiers(a,b):
    '''Prend deux entiers a et b avec a <= b et retourne la somme a+(a+1)+(a+2)+...+b'''
    res = 0                          # initialisation du résultat
    for k in range(a,b+1):           # boucle de calcul qui va augmenter res pas à pas
        res = res + k                # <--- AFFECTATION : "res devient égal à..."
    return res                       # et surtout pas print, qui se fera à l'extérieur !

Donc ici l'opérateur d'affectation = joue un rôle de modificateur et se lit "devient égal à". Ne pas le confondre avec l'égalité ==.

Les choses se compliquent lorsqu'une variable res existe déjà comme variable globale déclarée en-dehors de la fonction. Pas de souci : la nouvelle variable locale res va MASQUER la variable res située à l'extérieur. Vérifiez en live que la valeur de res extérieure n'a pas changé !

NB. Il y a un cas délicat : celui où res existe à l'extérieur, et vous souhaitez la modifier dans la fonction (beurk). Vous devez OBLIGATOIREMENT alors déclarer que res est GLOBALE avec le mot-clé global [PAA §2.17] :

res = 0

def augmenter_res_de(x):  # fonction sans résultat, à effet
    global res            # sinon la ligne suivante provoque une "UnboundLocalError", brrr
    res = res + x         # mais res = 3 sans global ferait de res une "locale", ATTENTION !!!

Donc Python n'aime pas les variables globales, et préfère les locales. C'est propre.

A partir de maintenant, on peut lâcher les rênes. Un boulevard algorithmique s'offre à vous, avec des multitudes d'exos. On essaye si possible d'avoir des garde-fous de programmation saine :
- Dans un fichier .py, éviter de faire des calculs en-dehors des fonctions. Pour résoudre un exo dont l'énoncé ne précise pas "Ecrire une fonction qui...", on pourra faire une fonction exo8() sans paramètre, exécutant un calcul et retournant la valeur attendue s'il y en a une, ou bien affichant le bilan des calculs faits, sans résultat. C'est toujours mieux d'avoir un résultat et de procéder à l'affichage en-dehors de la fonction, cela permettra de la ré-utiliser ultérieurement si besoin.
- Minimiser le nombre des constantes globales. Eviter les variables globales qui seraient modifiées à l'intérieur des fonctions.
- Documenter un minimum chaque fonction. De quels types sont les paramètres ? Quel est le résultat retourné s'il y en a un, sinon quel est l'effet ? Commenter avec # les lignes critiques pour aider le lecteur. N'oublions que la programmation est une activité de REDACTION dans une langue non naturelle !
- N'oublions pas non plus que l'enseignant qui commence à corriger de l'algorithmique n'aura pas toujours la tâche facile, il faut que l'élève l'aide, notamment en livrant un fichier .py qui pourra être exécuté par l'enseignant et ce fichier DEVRA contenir les tests pertinents sur les fonctions écrites. Les tests ne prouvent rien, mais ils donnent des indices et détectent souvent des erreurs courantes. Si un programme ne fonctionne pas, l'élève sera invité à le dire en commentaire dans son fichier, preuve qu'il aura testé son travail.

6.2 La compréhension de somme [PAA §2.27]

Sa présentation peut être reportée en classe de Première (où elle sera étendue), mais elle est envisageable si la classe accroche. Ses deux formes sont :

>>> sum(n for n in range(1,10))                 # 1 + 2 +...+ 9
45
>>> sum(k*k for k in range(1,10) if k%2 == 1)   # 1**2 + 3**2 +...+ 9**2
165

7. La boucle non bornée while [PAA §2.24]

C'est l'instruction TANT QUE en langage naturel. Elle est non bornée car on connaît pas a priori combien il y aura de tours de boucle. Elle est donc plus dangeureuse et peut tourner sans stopper si la condition d'arrêt n'est jamais réalisée. On lui préfère la boucle for si possible, mais il y a des cas où elle est indispensable. Son format est :

         while <condition>: 
             <instruction>
             ...

Une boucle for est un cas particulier de boucle while :

     for x in range(a,b):           <==>        x = a
         print(x,f(x))                          while x < b:
                                                    print(x,f(x))
                                                    x = x + 1

NB. Cette équivalence n'est pas correcte à 100%, voyez-vous pourquoi ?...

8. Les chaînes de caractères [PAA chap 3 sauf §3.5]

Ce sont les textes délimités par une apostrophe simple ' au début et à la fin. L'apostrophe peut être remplacée par un guillemet " à la Java, notamment si la chaîne contient... une apostrophe. Les caractères d'une chaîne s sont numérotés de gauche à droite en croissant à partir de 0 jusqu'à len(s)-1. Le caractère numéro i se note s[i]. On les parcourt parfois de droite à gauche en décroissant à partir de -1 jusqu'à -len(s).

>>> s = 'Bonjour à tous !'
>>> type(s)
<class 'str'>                # comme string...
>>> len(s)                   # longueur, nombre de caractères
16
>>> s[3]                     # le caractère numéro 3 de s
j
>>> s[-1]                    # le dernier caractère de s
'!'

L'opération importante est la concaténation ou juxtaposition de chaînes :

>>> s + ' Hello...'          # concaténation. Essayez 2 * 'Hello!'
'Bonjour à tous ! Hello...'

Pour boucler séquentiellement sur une chaîne (qui, comme range, est un objet itérable), il y a deux manières de rédiger la boucle for. Soit on boucle sur caractères eux-mêmes, si les indices ne jouent aucun rôle. Soit on boucle sur les indices (numéros à partir de 0) des caractères, si l'indice joue un rôle dans la solution. Exemples :

def nb_voyelles_car(s):      # str --> int, s est une chaîne en minuscules non accentuées
    res = 0
    for c in s:              # on boucle sur les caractères
        if c in 'aeiouy': res = res + 1
    return res

def nb_voyelles_car(s):      # en utilisant une compréhension de somme
    return sum(1 for c in s if c in 'aeiouy')

def position_ind(c,s):
    '''Retourne l'indice de la première apparition du caractère c dans la chaîne s,
    ou bien False'''
    for i in range(len(s)):  # on boucle sur les indices de 0 jusqu'à n-1
       if s[i] == c: return i      # échappement
    return False

La fonction str(x) de Python permet d'obtenir une représentation de l'objet x sous forme de chaîne de caractères. C'est le grand enchaîneur d'après certains (et int serait le libérateur pour rester entier alors ? Hum).

>>> str(-3456)               # int --> str              (et avec un float ?)
'-3456'
>>> int('-3456')             # str --> int              (et avec un float ?)
-3456

Cela permet souvent de boucler sur un nombre en parcourant ses chiffres de gauche à droite ou droite à gauche sous forme de caractères, au moyen d'une boucle [PAA exo 11.40], sans passer par quotient et modulo...

9. Le graphisme de la tortue [PAA chap 4]

Ô combien gratifiant sur le plan de la programmation car constructif et visuel, le graphisme de la tortue permet de s'entraîner aux boucles for et... aux fonctions sans résultats, dont l'EFFET est de laisser une trace sur l'écran. Mais rien n'empêche exceptionnellement, lorsqu'une fonction dessine un tracé, de rendre... la distance parcourue par la tortue par exemple. Les promenades aléatoires (tortue ivre) font la joie des petits et des grands, sans trigonométrie :-) Mais pour de véritables animations, mieux vaut utiliser Processing.py [PAA §4.2].

La tortue est un animal virtuel que l'on pilote avec un opérateur de translation forward(d) et un opérateur de rotation instantanée sur place left(a) où l'angle a est en degrés. Bref, elle effectue un déplacement dans le plan euclidien en dessinant une courbe. C'est foo ce qu'on peut tirer de cette petite idée géniale !

Mangez des oursins ! [PAA exo 11.52]

10. Les algorithmes

Sans doute faut-il faire découvrir la joie de programmer en Seconde, sans débuter une classification des algorithmes. Jouer avec des entiers, des chaînes de caractères, résoudre de petits problèmes de la vie courante. Il est bon d'habituer l'élève à parcourir quelque chose pour construire une solution. On parcourt les chiffres d'un entier, les éléments d'un intervalle range, les caractères d'une chaîne de manière essentiellement séquentielle (les uns après les autres, en ligne droite). Il sera bon de montrer un exemple de dichotomie en faisant sentir en quoi ça permet d'aller vite, les sensibiliser à l'efficacité (en temps) d'un algorithme. Comparer les temps de calcul de deux algorithmes pour un même problème avec le module time est éclairant pour des jeunes qui passent du temps dans les jeux vidéo ou subissent la lenteur des réseaux.

La joie de programmer n'empêche pas la rigueur. On reste dans les mathématiques, mais avec un esprit calculateur, constructif et créateur, qui gomme un peu l'esprit spartiate et quelque peu scholastique des maths ressenti par certains élèves. Et surtout : on apprend à une machine à calculer, et non l'inverse :-)

Les livres de maths et d'activités de Seconde regorgent d'exercices. En ce qui concerne [PAA], voici une petite sélection extraite du chapitre 11 (Vladivostok), à simplifier au besoin pour la classe de Seconde. Tous les exercices sont corrigés en ligne sur :

http://jean.paul.roy.free.fr/PAA/sols.html

- Nombres : 11.1, 11.2 (a-b-c), 11.2 (d-e sur le binaire, optionnel).

- Fonctions première approche : 11.4 (sans if), 11.5, 11.6, 11.7, 11.8

- Fonctions avec if : 11.4 (a), 11.10, 11.12

- Fonctions avec boucles (pas de récurrence) : 11.16, 11.17 (sauf c), 11.20, 11.25, 11.37, 11.38, 11.40 (binaire), 11.44, 11.45, 11.50, 11.51, 11.52, 11.55, 11.57.