1. Initiation à Python🔗

1.1. Types de base🔗

  • None (rien)

  • Chaînes de caractères: str

    • Entre (simples ou triples) apostrophes ' ou guillemets ": 'Calvin', "Calvin'n'Hobbes", '''Deux\nlignes''', """'Pourquoi?' demanda-t-il."""

    • Conversion: str(3.2)

  • Types numériques:

    • Booléens bool (vrai/faux): True, False, bool(3)

    • Entiers int (pas de valeur limite explicite, correspond au moins au long du C): -2, int(2.1), int("4")

    • Réels float (entre ±1.7e±308, correspond au double du C): 2., 3.5e-6, float(3)

    • Complexes complex: 1+2j (sans espace), 5.1j, complex(-3.14), complex('j')

    >>> 5 / 2       # Division réelle par défaut dans Python 3.x
    2.5
    >>> 6 // 2.5    # Division euclidienne explicite
    2.0
    >>> 6 % 2.5     # Reste de la division euclidienne
    1.0
    >>> (1 + 2j)**-0.5  # Puissance entière, réelle ou complexe
    (0.5688644810057831-0.3515775842541429j)
    
  • Objets itérables:

    • Listes list: ['a', 3, [1, 2], 'a']

    • Listes immuables tuple: (2, 3.1, 'a', []) (selon les conditions d'utilisation, les parenthèses ne sont pas toujours nécessaires)

    • Listes à clés dict: {'a':1, 'b':[1, 2], 3:'c'}

    • Ensembles non ordonnés d'éléments uniques set: {1, 2, 3, 2}

    >>> l = ['a', True]  # Définition d'une liste
    >>> x, y = 1, 2.5    # Affectations multiples via tuples (les parenthèses ne sont pas nécessaires)
    >>> list(range(5))   # Liste de 5 entiers commençant par 0
    [0, 1, 2, 3, 4]
    >>> l + [x, y]       # Concaténation de listes
    ['a', True, 1, 2.5]
    >>> {2, 1, 3} | {1, 2, 'a'}  # Union d'ensembles (non-ordonnés)
    {'a', 1, 2, 3}
    

    Attention

    En Python 3, range() n'est plus un constructeur de liste, mais un itérateur, qui doit être converti en liste explicitement (équivalent à xrange de Python 2):

    >>> range(3)        # Itérateur
    range(0, 3)
    >>> list(range(3))  # Liste
    [0, 1, 2]
    
  • type(obj) retourne le type de l'objet, isinstance(obj, type) teste le type de l'objet.

    >>> type(l)
    <type 'list'>
    >>> isinstance(l, tuple)
    False
    

Liens:

1.2. Structures de programmation🔗

  • Les blocs sont définis par l'indentation (en général par pas de quatre espaces) [1].

    Avertissement

    Évitez autant que possible les caractères de tabulation, source de confusion. Configurez votre éditeur de texte pour qu'il n'utilise que des espaces.

  • Une instruction par ligne en général (ou instructions séparées par ;).

  • Les commentaires commencent par #, et s'étendent jusqu'à la fin de la ligne.

  • Expression booléenne: une condition est une expression s'évaluant à True ou False:

    • False: test logique faux (p.ex. 3 > 4), valeur nulle, chaîne vide (''), liste vide ([]), etc.

    • True: test logique vrai (p.ex. 2 in [1, 2, 3]), toute valeur ou objet non nul (et donc s'évaluant par défaut à True sauf exception)

    • Tests logiques: ==, !=, >, >=, in, etc.

      Attention

      Ne pas confondre « = » (affectation d'une variable) et « == » (test logique d'égalité).

    • Opérateurs logiques: and, or, not

      >>> x = 3
      >>> not ((x <= 0) or (x > 5))
      True
      >>> 0 < x <= 5   # Conditions chaînées
      True
      
    • Opérateur ternaire (PEP 308): value if condition else altvalue, p.ex.

      >>> y = x**0.5 if (x > 0) else 0  # Retourne sqrt(max(x, 0))
      
  • Expression conditionnelle: if condition: ... [elif condition2: ...] [else: ...], p.ex.:

    if (i > 0):    # Condition principale
        print("positif")
    elif (i < 0):  # Condition secondaire (si nécessaire)
        print("négatif")
    else:          # Cas final (si nécessaire)
        print("nul")
    
  • Boucle for: for element in iterable:, s'éxecute sur chacun des éléments d'un objet itérable:

    >>> for val in ['un', (2, 3), 4]:  # Itération sur une liste de 3 éléments
    ...     print(val)
    un
    (2, 3)
    4
    
    • continue: interrompt l'itération courante, et reprend la boucle à l'itération suivante,

    • break: interrompt complètement la boucle.

    Note

    la logique des boucles Python est assez différente des langages C[++]/fortran, pour lesquels l'itération porte sur les indices plutôt que sur les éléments eux-mêmes.

  • Boucle while: while condition: se répéte tant que la condition est vraie, ou jusqu'à une sortie explicite avec break.

    Attention

    aux boucles infinies, dont la condition d'exécution reste invariablement vraie (typiquement un critère de convergence qui n'est jamais atteint). On peut toujours s'en protéger en testant en outre sur un nombre maximal (raisonnable) d'itérations:

    niter = 0
    while (error > 1e-6) and (niter < 100):
        error = ...   # A priori, error va décroître, et la boucle s'interrompre...
        niter += 1    # ... mais on n'est jamais assez prudent!
    if niter == 100:  # Ne pas oublier de tester l'absence de convergence!!!
        print("Erreur de convergence!")
    

Note

Il n'y a pas en Python d'équivalent natif à l'instruction switch du C [4], ni à la structure do expression while condition; cette dernière peut être remplacée par:

while True:
    # calcul de la condition d'arrêt
    if condition:
        break

Exercices:

Intégration: méthode des rectangles ★, Fizz Buzz ★, PGCD: algorithme d'Euclide ★★

1.3. Les chaînes de caractères🔗

1.3.1. Indexation🔗

Les chaînes de caractères sont des objets itérables – c.-à-d. constitués d'éléments (ici les caractères) sur lesquels il est possible de « boucler » (p.ex. avec for) – et immuables – c.-à-d. dont les éléments individuels ne peuvent pas être modifiés intrinsèquement.

Note

Comme en C[++], l'indexation en Python commence à 0: le 1er élément d'une liste est l'élément n°0, le 2e est le n°1, etc. Les n éléments d'une liste sont donc indexés de 0 à n-1.

>>> alpha = 'abcdefghijklmnopqrstuvwxyz'
>>> len(alpha)
26
>>> alpha[0]    # 1er élément (l'indexation commence à 0)
'a'
>>> alpha[-1]   # = alpha[26-1=25], dernier élément (-2: avant-dernier, etc.)
'z'

1.3.2. Sous-liste (slice)🔗

Des portions d'une chaîne peuvent être extraites en utilisant des slice (« tranches »), de notation générique [start=0]:[stop=len][:step=1]. P.ex.

>>> alpha[3:7]  # De l'élément n°3 (inclus) au n°7 (exclu), soit 7-3=4 éléments
'defg'
>>> alpha[:3]   # Du n°0 (défaut) au n°3 (exclu), soit 3 éléments
'abc'
>>> alpha[-3:]  # Du n°26-3=23 (inclus) au dernier inclus (défaut)
'xyz'
>>> alpha[3:9:2]  # Du n°3 (inclus) au n°9 (exclu), tous les 2 éléments
'dfh'
>>> alpha[::5]    # Du 1er au dernier élément (défauts), tous les 5 éléments
'afkpuz'

1.3.3. Méthodes🔗

Comme la plupart des objets en Python, les chaînes de caractères disposent de nombreuses fonctionnalités – appelées « méthodes » en POO – facilitant leur manipulation:

>>> enfant, peluche = "Calvin", 'Hobbes'      # Affectations mutiples
>>> titre = enfant + ' et ' + peluche; titre  # +: Concaténation de chaînes
'Calvin et Hobbes'
>>> titre.replace('et', '&')  # Remplacement de sous-chaînes (→ nouvelle chaîne)
'Calvin & Hobbes'
>>> titre                     # titre est immuable et reste inchangé
'Calvin et Hobbes'
>>> ' & '.join(titre.split(' et ')) # Découpage (split) et jonction (join)
'Calvin & Hobbes'
>>> 'Hobbes' in titre               # in: Test d'inclusion
True
>>> titre.find("Hobbes")            # str.find: Recherche de sous-chaîne
10
>>> titre.center(30, '-')
'-------Calvin et Hobbes-------'
>>> dir(str)                        # Liste toutes les méthodes des chaînes
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

1.3.4. Formatage🔗

Le système de formatage permet un contrôle précis de la conversion de variables en chaînes de caractères. Après quelques tergiversations historiques [2], le système de choix est dorénavant (Python 3.6+) celui de la chaîne formatée (f-string), qui interprète directement les éléments du type {var[:format]} dans une chaîne:

>>> nom, age = 'calvin', 6
>>> f"{nom} a {age} ans."                                      # Interpolation simple
'calvin a 6 ans.'
>>> f"L'année prochaine, {nom.capitalize()} aura {age+1} ans"  # Interprétation
"L'année prochaine, Calvin aura 7 ans."
>>> f"{nom=}, {nom.capitalize()=}, {age=}, {age+1=}"           # Mode debug (3.8+)
"nom='calvin', nom.capitalize()='Calvin', age=6, age+1=7"

Le formatage des chaînes hérite de la grammaire standard du C:

>>> f"{nom:10s}, {nom:->10s}, {nom:!<10s}, {nom:=^10s}"     # Formatage de chaîne, ...
"calvin    , ----calvin, calvin!!!!, ==calvin=="
>>> f"{3} fois {7:03d} font {3*7:_<+5d}"                    # ..., d'entier, ...
'3 fois 007 font +21__'
>>> pi = 3.1415926535897931
>>> f"{pi=}, {pi:f}, {pi:·<+9.3f}, {pi:.3g}, {pi*1e3:.2f}, {pi*1e3:.3g}"  # ..., de réel.
'pi=3.141592653589793, 3.141593, +3.142···, 3.14, 3141.59, 3.14e+03'

print() affiche à l'écran (plus spécifiquement la sortie standard par défaut) la conversion d'une variable en chaîne de caractères:

>>> print("Calvin and Hobbes\nScientific progress goes 'boink'!")
Calvin and Hobbes
Scientific progress goes 'boink'!
>>> z = 1 + 2j; print(z)  # = print(str(z))
(1+2j)

Exercice:

Tables de multiplication ★

1.4. Objets itérables🔗

Les chaînes de caractères, listes, tuples et dictionnaires sont les objets itérables de base en Python. Les listes et dictionnaires sont modifiables (« mutables ») – leurs éléments constitutifs peuvent être changés à la volée – tandis que chaînes de caractères et les tuples sont immuables.

  • Accès indexé: conforme à celui des chaînes de caractères

    >>> l = list(range(1, 10, 2)); l  # De 1 (inclus) à 10 (exclu) par pas de 2
    [1, 3, 5, 7, 9]
    >>> len(l)          # Nb d'éléments dans la liste (i varie de 0 à 4)
    5
    >>> l[0], l[-2]     # 1er et avant-dernier élément (l'indexation commence à 0)
    (1, 7)
    >>> l[5]            # Erreur: indice hors-bornes
    IndexError: list index out of range
    >>> d = dict(a=1, b=2)  # Création du dictionnaire {'a':1, 'b':2}
    >>> d['a']          # Accès à une entrée via sa clé
    1
    >>> d['c']          # Erreur: clé inexistante!
    KeyError: 'c'
    >>> d['c'] = 3; d   # Ajout d'une clé et sa valeur
    {'a': 1, 'c': 3, 'b': 2}
    >>> # Noter qu'un dictionnaire N'est PAS ordonné!
    

    Note

    Depuis Python 3.7 (en pratique depuis 3.6), un dictionnaire est ordonné, i.e., conserve l'ordre d'insertion (ce n'était pas le cas dans l'implémentation historique).

  • Sous-listes (slices):

    >>> l[1:-1]        # Du 2e ('1') *inclus* au dernier ('-1') *exclu*
    [3, 5, 7]
    >>> l[1:-1:2]      # Idem, tous les 2 éléments
    [3, 7]
    >>> l[::2]         # Tous les 2 éléments (*start=0* et *stop=len* par défaut)
    [1, 5, 9]
    
  • Modification d'éléments d'une liste (chaînes et tuples sont immuables):

    >>> l[0] = 'a'; l            # Remplacement du 1er élément
    ['a', 3, 5, 7, 9]
    >>> l[1::2] = ['x', 'y']; l  # Remplacement d'éléments par *slices*
    ['a', 'x', 5, 'y', 9]
    >>> l + [1, 2]; l            # Concaténation (l reste inchangé)
    ['a', 'x', 5, 'y', 9, 1, 2]
    ['a', 'x', 5, 'y', 9]
    >>> l += [1, 2]; l           # Concaténation sur place (l est modifié)
    ['a', 'x', 5, 'y', 9, 1, 2]
    >>> l.append('z'); l         # Ajout d'un élément en fin de liste
    ['a', 'x', 5, 'y', 9, 1, 2, 'z']
    >>> l.extend([-1, -2]); l    # Extension par une liste
    ['a', 'x', 5, 'y', 9, 1, 2, 'z', -1, -2]
    >>> del l[-6:]; l            # Efface les 6 derniers éléments de la liste
    ['a', 'x', 5, 'y']
    

    Attention

    à la modification des objets mutables:

    >>> l = [0, 1, 2]
    >>> m = l; m  # m est un *alias* de la liste l: c'est le même objet
    [0, 1, 2]
    >>> id(l); id(m); m is l
    171573452     # id({obj}) retourne le n° d'identification en mémoire
    171573452     # m et l ont le même id:
    True          # ils correspondent donc bien au même objet en mémoire
    >>> l[0] = 'a'; m  # puisque l a été modifiée, il en est de même de m
    ['a', 1, 2]
    >>> m = l[:]  # copie tous les elts de l dans une *nouvelle* liste m (clonage)
    >>> id(l); id(m); m is l
    171573452
    171161228     # m a un id différent de l: il s'agit de 2 objets distincts
    False         # (contenant éventuellement la même chose!)
    >>> del l[-1]; m  # les éléments de m n'ont pas été modifiés
    ['a', 1, 2]
    
  • Liste en compréhension [ expression for element in iterable ]: elle permet la construction d'une liste à la volée

    >>> [ i**2 for i in range(5) ]  # Carré de tous les éléments de [0, ..., 4]
    [0, 1, 4, 9, 16]
    >>> [ 2*i for i in range(10) if (i%3 != 0) ]  # Compréhension conditionnelle
    [2, 4, 8, 10, 14, 16]
    >>> [ 10*i+j for i in range(3) for j in range(4) ]     # Double compréhension
    [0, 1, 2, 3, 10, 11, 12, 13, 20, 21, 22, 23]
    >>> [ [ 10*i+j for i in range(3) ] for j in range(4) ] # Compréhensions imbriquées
    [[0, 10, 20], [1, 11, 21], [2, 12, 22], [3, 13, 23]]
    

    Attention

    Ne pas inclure de calculs inutiles dans la liste en compréhension!:

    [ calcul(x)**i for i in range(5) ]  # Argh, `calcul(x)` sera exécuté 5 fois!
    y = calcul(x); [ y**i for i in range(5) ]  # Bien mieux!
    
  • Dictionnaire en compréhension {key: value for element in iterable }: de manière similaire, elle permet la construction d'un dictionnaire à la volée

    >>> { i: bin(i) for i in range(5) }   # Dictionnaire {i: représentation binaire}
    {0: '0b0', 1: '0b1', 2: '0b10', 3: '0b11', 4: '0b100'}
    
  • Utilitaires sur les itérables:

    >>> humans = ['Calvin', 'Wallace', 'Boule']
    >>> for i in range(len(humans)):  # Boucle sur les indices de humans
    ...     print(i, humans[i])       # Accès explicite, pas pythonique :-(
    0 Calvin
    1 Wallace
    2 Boule
    >>> for i, name in enumerate(humans):  # Boucle sur (indice, valeur) de humans
    ...     print(i, name)                 # Pythonique :-D
    0 Calvin
    1 Wallace
    2 Boule
    >>> animals = ['Hobbes', 'Gromit', 'Bill']
    >>> for boy, dog in zip(humans, animals):  # Boucle simultanée sur 2 listes (ou +)
    ...     print(boy, 'et', dog)
    Calvin et Hobbes
    Wallace et Gromit
    Boule et Bill
    >>> sorted(zip(humans, animals))  # Tri, ici sur le 1er élément de chaque tuple de la liste
    [('Boule', 'Bill'), ('Calvin', 'Hobbes'), ('Wallace', 'Gromit')]
    

Exercices:

Crible d'Ératosthène ★, Carré magique ★★

1.5. Fonctions🔗

Une fonction est un regroupement d'instructions impératives – assignations, branchements, boucles, etc. – s'appliquant sur des arguments d'entrée. C'est le concept central de la programmation impérative.

def permet de définir une fonction: def fonction(arg1, arg2, ..., option1=valeur1, option2=valeur2, ...):. Les « args » sont des arguments nécessaires (c.-à-d. obligatoires), tandis que les « kwargs » – arguments de type option=valeur – sont optionnels, puisqu'ils possèdent une valeur par défaut. Si la fonction doit retourner une valeur, celle-ci est spécifiée par le mot-clé return.

Exemples:

 1def temp_f2c(tf):
 2    """
 3    Convertit une température en d° Fahrenheit `tf` en d° Celsius.
 4
 5    Exemple:
 6    >>> temp_f2c(104)
 7    40.0
 8    """
 9
10    tc = (tf - 32.)/1.8       # Fahrenheit → Celsius
11
12    return tc

Dans la définition d'une fonction, la première chaîne de charactères (appelé docstring) servira de documentation pour la fonction, accessible de l'interpréteur via p.ex. help(temp_f2c), ou temp_f2c? sous ipython. Elle se doit d'être tout à la fois pertinente, concise et complète. Elle peut également inclure des exemples d'utilisation (doctests, voir Développement piloté par les tests).

 1def mean_power(alist, power=1):
 2    r"""
 3    Retourne la racine `power` de la moyenne des éléments de `alist` à
 4    la puissance `power`:
 5
 6    .. math:: \mu = (\frac{1}{N}\sum_{i=0}^{N-1} x_i^p)^{1/p}
 7
 8    `power=1` correspond à la moyenne arithmétique, `power=2` au *Root
 9    Mean Squared*, etc.
10
11    Exemples:
12    >>> mean_power([1, 2, 3])
13    2.0
14    >>> mean_power([1, 2, 3], power=2)
15    2.160246899469287
16    """
17
18    # *mean* = (somme valeurs**power / nb valeurs)**(1/power)
19    mean = (sum( val ** power for val in alist ) / len(alist)) ** (1 / power)
20
21    return mean

Il faut noter plusieurs choses importantes:

  • Python est un langage à typage dynamique, p.ex., le type des arguments d'une fonction n'est pas fixé a priori. Dans l'exemple précédent, alist peut être une list, un tuple ou tout autre itérable contenant des éléments pour lesquels les opérations effectuées – somme, exponentiation, division par un entier – ont été préalablement définies (p.ex. des entiers, des complexes, des matrices, etc.): c'est ce que l'on appelle le duck-typing [3], favorisant le polymorphisme des fonctions;

  • le typage est fort, c.-à-d. que le type d'une variable ne peut pas changer à la volée. Ainsi, "abra" + "cadabra" a un sens (concaténation de chaînes), mais pas 1 + "2" ou 3 + "cochons" (entier + chaîne);

  • la définition d'une fonction se fait dans un « espace parallèle » où les variables ont une portée (scope) locale [5]. Ainsi, la variable s définie dans la fonction mean_power n'interfère pas avec le « monde extérieur » ; inversement, la définition de mean_power ne connaît a priori rien d'autre que les variables explicitement définies dans la liste des arguments ou localement.

Pour les noms de variables, fonctions, etc. utilisez de préférence des caractères purement ASCII [8] (a-zA-Z0-9_); de manière générale, favorisez plutôt la langue anglaise (variables, commentaires, affichages).

Exercice:

Suite de Syracuse (fonction) ★

1.6. Bibliothèques et scripts🔗

1.6.1. Bibliothèques externes🔗

Une bibliothèque est un ensemble d'instructions fournissant des fonctionnalités supplémentaires – p.ex. des constantes ou fonctions prédéfinies – au langage Python. Ainsi, la bibliothèque math définit les fonctions et constantes mathématiques usuelles (sqrt(), pi, etc.)

Une bibliothèque est « importée » avec la commande import module. Les fonctionnalités supplémentaires sont alors accessibles dans l'espace de noms module via module.fonction:

>>> sqrt(2)                   # sqrt n'est pas une fonction standard de python
NameError: name 'sqrt' is not defined
>>> import math               # Importe le module 'math'
>>> dir(math)                 # Liste les fonctionnalités de 'math'
['__doc__', '__name__', '__package__', 'acos', 'acosh', 'asin',
'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh',
'degrees', 'e', 'exp', 'fabs', 'factorial', 'floor', 'fmod', 'frexp',
'fsum', 'hypot', 'isinf', 'isnan', 'ldexp', 'log', 'log10', 'log1p',
'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh',
'trunc']
>>> math.sqrt(math.pi)        # Les fonctionnalités sont disponibles sous 'math'
1.7724538509055159
>>> import math as M          # Importe 'math' avec l'alias 'M'
>>> M.sqrt(M.pi)
1.7724538509055159
>>> from math import sqrt, pi # Importe uniquement 'sqrt' et 'pi' dans l'espace courant
>>> sqrt(pi)
1.7724538509055159

Avertissement

Il est possible d'importer toutes les fonctionnalités d'une bibliothèque dans l'espace de noms courant:

>>> from math import *    # Argh! Pas pythonique :-(
>>> sqrt(pi)
1.7724538509055159

Cette pratique est cependant fortement déconseillée du fait des confusions dans les espaces de noms qu'elle peut entraîner:

>>> from cmath import *
>>> sqrt(-1)  # Quel sqrt: le réel (de math) ou le complexe (de cmath)?

Nous verrons par la suite quelques exemples de bibliothèques de la Bibliothèque standard, ainsi que des Bibliothèques numériques de base orientées analyse numérique.

Exercice:

Flocon de Koch (programmation récursive) ★★★

1.6.2. Bibliothèques personnelles et scripts🔗

Vous pouvez définir vos propres bibliothèques en regroupant les fonctionnalités au sein d'un même fichier monfichier.py (il s'agit alors d'un module).

  • Si ce fichier est importé (p.ex. import monfichier), il agira comme une bibliothèque;

  • si ce fichier est exécuté – p.ex. python ./monfichier.py – il agira comme un script.

Attention

Toutes les instructions d'un module qui ne sont pas encapsulées dans le __main__ (voir plus bas) sont interprétées et exécutées lors de l'import du module. Elles doivent donc en général se limiter à la définition de variables, de fonctions et de classes (en particulier, éviter les affichages ou les calculs longs).

Un code Python peut donc être:

  • un module, s'il n'inclut que des définitions mais pas d'instruction exécutable en dehors d'un éventuel __main__ [6];

  • un exécutable, s'il inclut un __main__ ou des instructions exécutables;

  • ou les deux à la fois.

Exemple:

Le code mean_power.py peut être importé comme une bibliothèque (p.ex. import mean_power) dans un autre code Python, ou bien être exécuté depuis la ligne de commande (p.ex. python mean_power.py), auquel cas la partie __main__ sera exécutée.

  • #! (Hash-bang): la première ligne d'un script défini l'interpréteur à utiliser [7]:

    #!/usr/bin/env python3
    
  • """doc""": la chaîne de documentation de la bibliothèque (docstring, PEP 257), qui sera utilisée comme aide en ligne du module (help(mean_power)), doit être la 1re instruction du script.

  • if __name__ == '__main__': permet de séparer le __main__ (c.-à-d. le corps du programme, à exécuter lors d'une utilisation en script) des définitions de fonctions et classes, permettant une utilisation en module.

1.7. Exceptions🔗

Comme nous avons pu le voir, l'interpréteur Python, lorsqu'il rencontre une erreur dans l'exécution d'une instruction, génère une Exception, de nature différente selon la nature de l'erreur: IndexError (p.ex. indice de liste invalide), KeyError (p.ex. clé de dictionnaire invalide), NameError (p.ex. variable ou fonction inconnue), IOError (p.ex. fichier inconnu), TypeError, ValueError, AttributeError, NotImplementedError, KeyboardInterrupt (p.ex. Ctrl-c), etc.

La nature exacte de l'exception est précisée dans la documentation de la fonction la générant, ou déterminée empiriquement:

>>> int("toto")  # la valeur de la chaîne ne permet pas la conversion en entier
ValueError: invalid literal for int() with base 10: 'toto'
>>> int([1,])    # le type list ne peut pas être converti en entier
TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'

La levée d'une erreur n'est cependant pas nécessairement fatale, puisque Python dispose d'un mécanisme avancé de gestion des erreurs.

Il est d'usage en Python d'utiliser la philosophie EAFP [9]: plutôt que de tester explicitement toutes les conditions de validité d'une instruction, on « tente sa chance » d'abord, quitte à gérer les erreurs a posteriori. Cette gestion des exceptions se fait par la construction try/except:

try:
    # instructions pouvant lever une exception
    ...
except Exception1:                 # Type d'exception interceptée
    # instructions à exécuter en cas d'exception de ce(s) type(s)
    ...
[except (Exception2, Exception3):  # Autres exceptions interceptées
    # instructions à exécuter en cas d'exception de ce(s) type(s)
    ...]
[else:
    # instructions à exécuter si *aucune* exception
    ...]
[finally:
    # instructions à exécuter dans tous les cas (avant de sortir du try/except)
    ...]

Dans l'élaboration d'un programme, gérez explicitement les erreurs que vous auriez pu tester a priori et pour lesquels il existe une solution de repli, et laissez passer les autres (ce qui provoquera éventuellement l'interruption légitime du programme).

Exemples:

try:
    y = math.sqrt(x)  # ValueError si x<0, TypeError si x n'est pas numérique
except ValueError:
    print(f"Avertissement: x={x} < 0, on impose y=-1")
    y = -1
# Note: l'exception TypeError n'est pas interceptée, parce qu'aucune
# solution de repli n'est prévue dans ce cas (x devrait être numérique,
# le pb est plus profond!)
1def lireEntier():
2    while True:  # Boucle infinie
3        chaine = input("Entrez un entier: ")  # Lecture du clavier → str
4        try:
5            # int() génère ValueError si la chaîne n'est pas coercible en entier
6            return int(chaine)  # Seule sortie possible de la boucle infinie
7        except ValueError:      # Gestion de l'exception ValueError
8            print(f"{chaine!r} n'est pas un entier")
>>> lireEntier()
Entrez un entier: toto
'toto' n'est pas un entier
Entrez un entier: 3,4
'3,4' n'est pas un entier
Entrez un entier: 4
4

Danger

Évitez à tout prix les except nus, c.-à-d. ne spécifiant pas la ou les exceptions à gérer, car ils intercepteraient alors toutes les exceptions, y compris celles que vous n'aviez pas prévues! Trouvez l'erreur dans le code suivant:

y = 2
try:
   x = z  # Copie y dans x
   print("Tout va bien")
except:   # Intercepte tout, y compris les erreurs légitimes!
   print("Rien ne va plus")

Vos procédures doivent également générer des exceptions (documentées) – avec l'instruction raise Exception() – si elles ne peuvent conclure leur action, à charge pour la procédure appelante de les gérer si besoin:

 1def diff_sqr(x, y):
 2    """
 3    Return x**2 - y**2 for x >= y, raise ValueError otherwise.
 4
 5    Exemples:
 6    >>> diff_sqr(5, 3)
 7    16
 8    >>> diff_sqr(3, 5)
 9    Traceback (most recent call last):
10    ...
11    ValueError: x=3 < y=5
12    """
13
14    if x < y:
15        raise ValueError(f"x={x} < y={y}")
16
17    return x**2 - y**2

Avant de se lancer dans un calcul long et complexe, on peut vouloir tester la validité de certaines hypothèses fondamentales, soit par une structure if ... raise, ou plus facilement à l'aide d'assert (qui, si l'hypothèse n'est pas vérifiée, génère une AssertionError):

1def diff_sqr(x, y):
2    """
3    Returns x**2 - y**2 for x >= y, AssertionError otherwise.
4    """
5
6    assert x >= y, f"x={x} < y={y}"  # Test et msg d'erreur
7
8    return x**2 - y**2

Note

La règle générale à retenir concernant la gestion des erreurs:

Fail early, fail often, fail better!

Exercice:

Jeu du plus ou moins (exceptions) ★

1.8. Classes🔗

Un objet est une entité de programmation, disposant de son propre état interne et de fonctionnalités associées: c'est le concept central de la Programmation Orientée Objet (POO).

Au concept d'objet sont liées les notions de:

  • Classe: il s'agit d'un modèle d'objet, dans lequel sont définis ses propriétés usuelles. P.ex. la classe Animal peut représenter un animal caractérisé par sa masse, et disposant de fonctionnalités propres, p.ex. grossir;

  • Instanciation: c'est le fait générer un objet concret (une instance) à partir d'un modèle (une classe). P.ex. vache = Animal(500.) crée une instance vache à partir de la classe Animal et d'une masse (float):

  • Attributs: variables internes décrivant l'état de l'objet. P.ex., vache.masse donne la masse de l'Animal vache;

  • Méthodes: fonctions internes, s'appliquant en premier lieu sur l'objet lui-même (self), décrivant les capacités de l'objet. P.ex. vache.grossit(10) modifie la masse de l'Animal vache;

    Attention

    Toutes les méthodes d'une classe doivent au moins prendre self – représentant l'instance même de l'objet – comme premier argument.

  • Surcharge d'opérateurs: cela permet de redéfinir les opérateurs et fonctions usuels (+, abs(), str(), etc.), pour simplifier l'écriture d'opérations sur les objets. Ainsi, on peut redéfinir les opérateurs de comparaison (<, >=, etc.) dans la classe Animal pour que les opérations du genre animal1 < animal2 aient un sens (p.ex. en comparant les masses).

  • Héritage de classe: il s'agit de définir une classe à partir d'une (ou plusieurs) classe(s) parente(s). La nouvelle classe hérite des attributs et méthodes de sa (ses) parente(s), que l'on peut alors modifier ou compléter. P.ex. la classe AnimalFeroce hérite de la classe Animal (elle partage la notion de masse), et lui ajoute des méthodes propres à la notion d'animal féroce (p.ex. dévorer un autre animal).

Exemple de définition de classe

 1class Animal:
 2    """
 3    Un animal, défini par sa `masse` et sa vitalité (`estVivant`).
 4    """
 5
 6    def __init__(self, masse):
 7        """
 8        Initialisation d'un Animal, a priori vivant.
 9
10        :param float masse: masse en kg (> 0)
11        :raise ValueError: masse non réelle ou négative
12        """
13
14        self.masse = float(masse)  # Ajout d'un attribut "masse"
15        if self.masse < 0:
16            raise ValueError("La masse ne peut pas être négative.")
17
18        self.estVivant = True      # Ajout d'un attribut "estVivant"
19
20
21    def __str__(self):
22        """
23        Surcharge de la fonction `str()`.
24
25        L'affichage *informel* de l'objet dans l'interpréteur, p.ex. `print(a)`
26        sera résolu comme `a.__str__()`
27
28        :return: une chaîne de caractères
29        """
30
31        return f"Animal {'vivant' if self.estVivant else 'mort'}, " \
32            f"{self.masse:.0f} kg"
33
34
35    def meurt(self):
36        """
37        L'animal meurt.
38        """
39
40        self.estVivant = False
41
42
43    def grossit(self, masse):
44        """
45        L'animal grossit (ou maigrit) d'une certaine masse (valeur algébrique).
46
47        :param float masse: prise (>0) ou perte (<0) de masse.
48        :raise ValueError: masse non réelle.
49        """
50
51        self.masse += float(masse)

Exemple d'héritage de classe

 1class AnimalFeroce(Animal):
 2    """
 3    Un animal féroce est un animal qui peut dévorer d'autres animaux.
 4
 5    La classe-fille hérite des attributs et méthodes de la
 6    classe-mère, mais peut les surcharger (i.e. en changer la
 7    définition), ou en ajouter de nouveaux:
 8
 9    - la méthode `AnimalFeroce.__init__()` dérive directement de
10      `Animal.__init__()` (même méthode d'initialisation);
11    - `AnimalFeroce.__str__()` surcharge `Animal.__str__()`;
12    - `AnimalFeroce.devorer()` est une nouvelle méthode propre à
13      `AnimalFeroce`.
14    """
15
16    def __str__(self):
17        """
18        Surcharge de la fonction `str()`.
19        """
20
21        return "Animal féroce " \
22            f"{'bien vivant' if self.estVivant else 'mais mort'}, " \
23            f"{self.masse:.0f} kg"
24
25    def devore(self, other):
26        """
27        L'animal (self) devore un autre animal (other).
28
29        * Si other est également un animal féroce, il faut que self soit plus
30          gros que other pour le dévorer. Sinon, other se défend et self meurt.
31        * Si self dévore other, other meurt, self grossit de la masse de other
32          (jusqu'à 10% de sa propre masse) et other maigrit d'autant.
33
34        :param Animal other: animal à dévorer
35        :return: prise de masse (0 si self meurt)
36        """
37
38        if isinstance(other, AnimalFeroce) and (other.masse > self.masse):
39            # Pas de chance...
40            self.meurt()
41            prise = 0.
42        else:
43            other.meurt()             # Other meurt
44            prise = min(other.masse, self.masse * 0.1)
45            self.grossit(prise)       # Self grossit
46            other.grossit(-prise)     # Other maigrit
47
48        return prise
 1class AnimalGentil(Animal):
 2    """
 3    Un animal gentil est un animal avec un `nom`.
 4
 5    La classe-fille hérite des attributs et méthodes de la
 6    classe-mère, mais peut les surcharger (i.e. en changer la
 7    définition), ou en ajouter de nouveaux:
 8
 9    - la méthode `AnimalGentil.__init__()` surcharge l'initialisation originale
10      `Animal.__init__()`;
11    - `AnimalGentil.__str__()` surcharge `Animal.__str__()`;
12    """
13
14    def __init__(self, masse, nom='Youki'):
15        """
16        Initialisation d'un animal gentil, avec son masse et son nom.
17        """
18
19        # Initialisation de la classe parente (nécessaire pour assurer
20        # l'héritage)
21        Animal.__init__(self, masse)
22
23        # Attributs propres à la classe AnimalGentil
24        self.nom = nom
25
26    def __str__(self):
27        """
28        Surcharge de la fonction `str()`.
29        """
30
31        return f"{self.nom}, un animal gentil " \
32            f"{'bien vivant' if self.estVivant else 'mais mort'}, " \
33            f"{self.masse:.0f} kg"
34
35    def meurt(self):
36        """
37        L'animal gentil meurt, avec un éloge funéraire.
38        """
39
40        Animal.meurt(self)
41        print(f"Pauvre {self.nom} meurt, paix à son âme...")

Note

Il est traditionnel d'écrire les noms de classes en CamelCase (AnimalGentil), et les noms d'instances de classe (les variables) en minuscules (vache).

Exemples

Animal (POO), Cercle circonscrit (POO, argparse)

Études de cas

Exercices:

Animaux (POO/TDD) ★, Jeu de la vie (POO) ★★

1.9. Entrées-sorties🔗

1.9.1. Intéractif🔗

Comme nous avons pu le voir précédemment, l'affichage à l'écran (sur la sortie standard) se fait par print, la lecture du clavier (entrée standard) par input.

Si nécessaire, l'affichage dans l'erreur standard (stderr) est possible:

>>> import sys
>>> print("Erreur!", file=sys.stderr)

1.9.2. Fichiers texte🔗

La gestion des fichiers (lecture et écriture) se fait à partir de la fonction open() retournant un objet de type file object:

 1# ========== ÉCRITURE ==========
 2outfile = open("carres.dat", 'w')   # Ouverture d'un fichier texte en écriture
 3for i in range(1, 10):
 4    outfile.write(f"{i}  {i**2}\n") # Noter la présence du '\n' (non-automatique)
 5outfile.close()  # Fermeture du fichier, absolument **NÉCESSAIRE** en écriture!
 6
 7# ========== LECTURE ==========
 8infile = open("carres.dat")  # Ouverture du fichier texte "carres.dat" en lecture
 9for line in infile:          # Boucle sur les lignes du fichier
10    if line.strip().startswith('#'): # Ne pas considérer les lignes "commentées"
11        continue
12    try:                     # Essayons de lire 2 entiers sur cette ligne
13        x, x2 = [ int(tok) for tok in line.split() ]  # ValueError possible
14    except ValueError:       # Gestion de l'erreur de conversion
15        print(f"Cannot decipher line {line!r}.")
16        continue
17    print(f"{x}**3 = {x**3}")
18infile.close()   # Fermeture du fichier, moins critique en lecture

Il est préconisé d'utiliser un gestionnaire de contexte (with, sur lequel je ne reviendrai pas), qui se charge en particulier de la fermeture automatique du fichier:

with open(...) as thefile:
    ...

Notes de bas de page