5. Fondamentaux de Python#

5.1. Vue d’ensemble#

Nous avons couvert beaucoup de matière assez rapidement, en nous concentrant sur des exemples.

Voyons maintenant certaines fonctionnalités fondamentales de Python de manière plus systématique.

Cette approche est moins passionnante mais aide à clarifier certains détails.

5.2. Types de données#

Les programmes informatiques suivent généralement toute une gamme de types de données.

Par exemple, 1.5 est un nombre à virgule flottante, tandis que 1 est un entier.

Les programmes doivent distinguer ces deux types pour diverses raisons.

L’une est qu’ils sont stockés différemment en mémoire.

Une autre est que les opérations arithmétiques sont différentes

  • Par exemple, l’arithmétique en virgule flottante est mise en œuvre sur la plupart des machines par une unité de calcul en virgule flottante (FPU) spécialisée.

En général, les flottants sont plus informatifs mais les opérations arithmétiques sur les entiers sont plus rapides et plus précises.

Python fournit de nombreux autres types de données Python intégrés, dont certains que nous avons déjà rencontrés

  • chaînes de caractères, listes, etc.

Apprenons-en un peu plus à leur sujet.

5.2.1. Types de données primitifs#

5.2.1.1. Valeurs booléennes#

Un type de données simple est celui des valeurs booléennes, qui peuvent être soit True, soit False

x = True
x
True

Nous pouvons vérifier le type de n’importe quel objet en mémoire à l’aide de la fonction type().

type(x)
bool

Dans la ligne de code suivante, l’interpréteur évalue l’expression à droite de = et lie y à cette valeur

y = 100 < 10
y
False
type(y)
bool

Dans les expressions arithmétiques, True est converti en 1 et False est converti en 0.

Cela s’appelle l”arithmétique booléenne et est souvent utile en programmation.

Voici quelques exemples

x + y
1
x * y
0
True + True
2
bools = [True, True, False, True]  # Liste de valeurs booléennes

sum(bools)
3

5.2.1.2. Types numériques#

Les types numériques sont également des types de données primitifs importants.

Nous avons déjà vu les types integer et float.

Les nombres complexes sont un autre type de données primitif en Python

x = complex(1, 2)
y = complex(2, 1)
print(x * y)

type(x)
5j
complex

5.2.2. Conteneurs#

Python possède plusieurs types de base pour stocker des collections de données (éventuellement hétérogènes).

Nous avons déjà abordé les listes.

Un type de données apparenté est celui des tuples, qui sont des listes « immuables »

x = ('a', 'b')  # Parenthèses au lieu des crochets
x = 'a', 'b'    # Ou sans crochets --- la signification est identique
x
('a', 'b')
type(x)
tuple

En Python, un objet est appelé immuable si, une fois créé, l’objet ne peut plus être modifié.

Inversement, un objet est mutable s’il peut encore être modifié après sa création.

Les listes Python sont mutables

x = [1, 2]
x[0] = 10
x
[10, 2]

Mais les tuples ne le sont pas

x = (1, 2)
x[0] = 10
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[13], line 2
      1 x = (1, 2)
----> 2 x[0] = 10

TypeError: 'tuple' object does not support item assignment

Nous en dirons davantage sur le rôle des données mutables et immuables un peu plus tard.

Les tuples (et les listes) peuvent être « déballés » comme suit

integers = (10, 20, 30)
x, y, z = integers
x
10
y
20

Vous avez en fait déjà vu un exemple de cela.

Le déballage de tuple est pratique et nous l’utiliserons souvent.

5.2.2.1. Notation de tranche#

Pour accéder à plusieurs éléments d’une séquence (une liste, un tuple ou une chaîne), vous pouvez utiliser la notation de tranche de Python.

Par exemple,

a = ["a", "b", "c", "d", "e"]
a[1:]
['b', 'c', 'd', 'e']
a[1:3]
['b', 'c']

La règle générale est que a[m:n] renvoie n - m éléments, en commençant par a[m].

Les nombres négatifs sont également autorisés

a[-2:]  # Les deux derniers éléments de la liste
['d', 'e']

Vous pouvez également utiliser le format [start:end:step] pour spécifier le pas

a[::2]
['a', 'c', 'e']

En utilisant un pas négatif, vous pouvez renvoyer la séquence dans l’ordre inverse

a[-2::-1] # Parcourir à rebours de l'avant-dernier élément jusqu'au premier élément
['d', 'c', 'b', 'a']

La même notation de tranche fonctionne sur les tuples et les chaînes

s = 'foobar'
s[-3:]  # Sélectionner les trois derniers éléments
'bar'

5.2.2.2. Ensembles et dictionnaires#

Deux autres types de conteneurs que nous devrions mentionner avant de poursuivre sont les ensembles et les dictionnaires.

Les dictionnaires ressemblent beaucoup aux listes, sauf que les éléments sont nommés au lieu d’être numérotés

d = {'name': 'Frodo', 'age': 33}
type(d)
dict
d['age']
33

Les noms 'name' et 'age' sont appelés les clés.

Les objets vers lesquels les clés sont mappées ('Frodo' et 33) sont appelés les values (valeurs).

Les ensembles sont des collections non ordonnées sans doublons, et les méthodes d’ensemble fournissent les opérations ensemblistes habituelles

s1 = {'a', 'b'}
type(s1)
set
s2 = {'b', 'c'}
s1.issubset(s2)
False
s1.intersection(s2)
{'b'}

La fonction set() crée des ensembles à partir de séquences

s3 = set(('foo', 'bar', 'foo'))
s3
{'bar', 'foo'}

5.3. Entrée et sortie#

Passons brièvement en revue la lecture et l’écriture dans des fichiers texte, en commençant par l’écriture

f = open('newfile.txt', 'w')   # Ouvrir 'newfile.txt' en écriture
f.write('Testing\n')           # Ici '\n' signifie nouvelle ligne
f.write('Testing again')
f.close()

Ici

  • La fonction intégrée open() crée un objet fichier pour écrire dedans.

  • write() et close() sont tous deux des méthodes des objets fichier.

Où se trouve ce fichier que nous avons créé ?

Rappelez-vous que Python maintient une notion de répertoire de travail courant (pwd) qui peut être localisé depuis Jupyter ou IPython via

%pwd
'/home/runner/work/lecture-python-programming.fr/lecture-python-programming.fr/lectures'

Si aucun chemin n’est spécifié, c’est là que Python écrit.

Nous pouvons également utiliser Python pour lire le contenu de newline.txt comme suit

f = open('newfile.txt', 'r')
out = f.read()
out
'Testing\nTesting again'
print(out)
Testing
Testing again

En fait, l’approche recommandée dans le Python moderne est d’utiliser une instruction with pour garantir que les fichiers sont correctement acquis et libérés.

Contenir les opérations dans le même bloc améliore aussi la clarté de votre code.

Note

Ce type de bloc est formellement appelé un contexte.

Essayons de convertir les deux exemples ci-dessus en une instruction with.

Nous modifions d’abord l’exemple d’écriture

with open('newfile.txt', 'w') as f:  
    f.write('Testing\n')         
    f.write('Testing again')

Notez que nous n’avons pas besoin d’appeler la méthode close() puisque le bloc with garantira que le flux est fermé à la fin du bloc.

Avec de légères modifications, nous pouvons également lire des fichiers en utilisant with

with open('newfile.txt', 'r') as fo:
    out = fo.read()
    print(out)
Testing
Testing again

Supposons maintenant que nous voulions lire une entrée depuis un fichier et écrire une sortie dans un autre. Voici comment nous pourrions accomplir cette tâche tout en acquérant et en restituant correctement les ressources au système d’exploitation à l’aide d’instructions with :

with open("newfile.txt", "r") as f:
    file = f.readlines()
    with open("output.txt", "w") as fo:
        for i, line in enumerate(file):
            fo.write(f'Line {i}: {line} \n')

Le fichier de sortie sera

with open('output.txt', 'r') as fo:
    print(fo.read())
Line 0: Testing
 
Line 1: Testing again

Nous pouvons simplifier l’exemple ci-dessus en regroupant les deux instructions with sur une seule ligne

with open("newfile.txt", "r") as f, open("output2.txt", "w") as fo:
        for i, line in enumerate(f):
            fo.write(f'Line {i}: {line} \n')

Le fichier de sortie sera identique

with open('output2.txt', 'r') as fo:
    print(fo.read())
Line 0: Testing
 
Line 1: Testing again

Supposons que nous voulions continuer à écrire dans le fichier existant au lieu de l’écraser.

nous pouvons passer au mode a qui signifie mode d’ajout (append)

with open('output2.txt', 'a') as fo:
    fo.write('\nThis is the end of the file')
with open('output2.txt', 'r') as fo:
    print(fo.read())
Line 0: Testing
 
Line 1: Testing again 

This is the end of the file

Note

Notez que nous n’avons couvert ici que les modes r, w et a, qui sont les modes les plus couramment utilisés. Python fournit une variété de modes que vous pourriez expérimenter.

5.3.1. Chemins#

Notez que si newfile.txt ne se trouve pas dans le répertoire de travail courant, alors cet appel à open() échoue.

Dans ce cas, vous pouvez déplacer le fichier vers le pwd ou spécifier le chemin complet vers le fichier

f = open('insert_full_path_to_file/newfile.txt', 'r')

5.4. Itération#

L’une des tâches les plus importantes en informatique est de parcourir une séquence de données et d’effectuer une action donnée.

L’une des forces de Python est son interface simple et flexible pour ce type d’itération via la boucle for.

5.4.1. Boucler sur différents objets#

De nombreux objets Python sont « itérables », dans le sens où l’on peut boucler dessus.

Pour donner un exemple, écrivons le fichier us_cities.txt, qui liste des villes américaines et leur population, dans le répertoire de travail courant.

%%writefile us_cities.txt
new york: 8244910
los angeles: 3819702
chicago: 2707120
houston: 2145146
philadelphia: 1536471
phoenix: 1469471
san antonio: 1359758
san diego: 1326179
dallas: 1223229
Overwriting us_cities.txt

Ici %%writefile est une commande magique de cellule IPython.

Supposons que nous voulions rendre l’information plus lisible, en mettant les noms en majuscules et en ajoutant des virgules pour marquer les milliers.

Le programme ci-dessous lit les données et effectue la conversion :

data_file = open('us_cities.txt', 'r')
for line in data_file:
    city, population = line.split(':')         # Déballage de tuple
    city = city.title()                        # Mettre en majuscule les noms de ville
    population = f'{int(population):,}'        # Ajouter des virgules aux nombres
    print(city.ljust(15) + population)
data_file.close()
New York       8,244,910
Los Angeles    3,819,702
Chicago        2,707,120
Houston        2,145,146
Philadelphia   1,536,471
Phoenix        1,469,471
San Antonio    1,359,758
San Diego      1,326,179
Dallas         1,223,229

Ici f' est une f-string utilisée pour insérer des variables dans des chaînes.

Le reformatage de chaque ligne est le résultat de trois méthodes de chaîne différentes, dont les détails peuvent être laissés pour plus tard.

La partie intéressante de ce programme pour nous est la ligne 2, qui montre que

  1. L’objet fichier data_file est itérable, dans le sens où il peut être placé à droite de in dans une boucle for.

  2. L’itération parcourt chaque ligne du fichier.

Cela conduit à la syntaxe propre et pratique présentée dans notre programme.

De nombreux autres types d’objets sont itérables, et nous en aborderons certains plus tard.

5.4.2. Boucler sans indices#

Une chose que vous avez peut-être remarquée est que Python tend à privilégier les boucles sans indexation explicite.

Par exemple,

x_values = [1, 2, 3]  # Un x itérable
for x in x_values:
    print(x * x)
1
4
9

est préféré à

for i in range(len(x_values)):
    print(x_values[i] * x_values[i])
1
4
9

Lorsque vous comparez ces deux alternatives, vous pouvez voir pourquoi la première est préférée.

Python fournit certaines facilités pour simplifier les boucles sans indices.

L’une est zip(), qui est utilisée pour parcourir des paires provenant de deux séquences.

Par exemple, essayez d’exécuter le code suivant

countries = ('Japan', 'Korea', 'China')
cities = ('Tokyo', 'Seoul', 'Beijing')
for country, city in zip(countries, cities):
    print(f'The capital of {country} is {city}')
The capital of Japan is Tokyo
The capital of Korea is Seoul
The capital of China is Beijing

La fonction zip() est également utile pour créer des dictionnaires — par exemple

names = ['Tom', 'John']
marks = ['E', 'F']
dict(zip(names, marks))
{'Tom': 'E', 'John': 'F'}

Si nous avons réellement besoin de l’indice d’une liste, une option est d’utiliser enumerate().

Pour comprendre ce que fait enumerate(), considérez l’exemple suivant

letter_list = ['a', 'b', 'c']
for index, letter in enumerate(letter_list):
    print(f"letter_list[{index}] = '{letter}'")
letter_list[0] = 'a'
letter_list[1] = 'b'
letter_list[2] = 'c'

5.4.3. Compréhensions de liste#

Nous pouvons également simplifier considérablement le code pour générer la liste de tirages aléatoires en utilisant quelque chose appelé une compréhension de liste.

Les compréhensions de liste sont un outil Python élégant pour créer des listes.

Considérez l’exemple suivant, où la compréhension de liste se trouve du côté droit de la deuxième ligne

animals = ['dog', 'cat', 'bird']
plurals = [animal + 's' for animal in animals]
plurals
['dogs', 'cats', 'birds']

Voici un autre exemple

range(8)
range(0, 8)
doubles = [2 * x for x in range(8)]
doubles
[0, 2, 4, 6, 8, 10, 12, 14]

5.5. Comparaisons et opérateurs logiques#

5.5.1. Comparaisons#

De nombreux types d’expressions différents s’évaluent à l’une des valeurs booléennes (c’est-à-dire True ou False).

Un type courant est celui des comparaisons, telles que

x, y = 1, 2
x < y
True
x > y
False

L’une des belles fonctionnalités de Python est que nous pouvons chaîner les inégalités

1 < 2 < 3
True
1 <= 2 <= 3
True

Comme nous l’avons vu précédemment, pour tester l’égalité nous utilisons ==

x = 1    # Affectation
x == 2   # Comparaison
False

Pour « différent de » utilisez !=

1 != 2
True

Notez que lors du test de conditions, nous pouvons utiliser n’importe quelle expression Python valide

x = 'yes' if 42 else 'no'
x
'yes'
x = 'yes' if [] else 'no'
x
'no'

Que se passe-t-il ici ?

La règle est :

  • Les expressions qui s’évaluent à zéro, à des séquences ou conteneurs vides (chaînes, listes, etc.) et None sont toutes équivalentes à False.

    • par exemple, [] et () sont équivalents à False dans une clause if

  • Toutes les autres valeurs sont équivalentes à True.

    • par exemple, 42 est équivalent à True dans une clause if

5.5.2. Combiner des expressions#

Nous pouvons combiner des expressions à l’aide de and, or et not.

Ce sont les connecteurs logiques standard (conjonction, disjonction et négation)

1 < 2 and 'f' in 'foo'
True
1 < 2 and 'g' in 'foo'
False
1 < 2 or 'g' in 'foo'
True
not True
False
not not True
True

Rappelez-vous

  • P and Q est True si les deux sont True, sinon False

  • P or Q est False si les deux sont False, sinon True

Nous pouvons également utiliser all() et any() pour tester une séquence d’expressions

all([1 <= 2 <= 3, 5 <= 6 <= 7])
True
all([1 <= 2 <= 3, "a" in "letter"])
False
any([1 <= 2 <= 3, "a" in "letter"])
True

Note

  • all() renvoie True lorsque toutes les valeurs/expressions booléennes de la séquence sont True

  • any() renvoie True lorsque l’une quelconque des valeurs/expressions booléennes de la séquence est True

5.6. Style de codage et documentation#

Un style de codage cohérent et l’utilisation de la documentation peuvent rendre le code plus facile à comprendre et à maintenir.

5.6.1. Directives de style Python : PEP8#

Vous pouvez trouver la philosophie de programmation de Python en tapant import this à l’invite de commande.

Entre autres choses, Python favorise fortement la cohérence dans le style de programmation.

Nous avons tous entendu le dicton sur la cohérence et les petits esprits.

En programmation, comme en mathématiques, le contraire est vrai

  • Un article mathématique où les symboles \(\cup\) et \(\cap\) seraient inversés serait très difficile à lire, même si l’auteur vous le disait à la première page.

En Python, le style standard est exposé dans PEP8.

(Occasionnellement, nous nous écarterons de PEP8 dans ces cours pour mieux correspondre à la notation mathématique)

5.6.2. Docstrings#

Python dispose d’un système pour ajouter des commentaires aux modules, classes, fonctions, etc. appelé docstrings.

Ce qui est agréable avec les docstrings, c’est qu’elles sont disponibles au moment de l’exécution.

Essayez d’exécuter ceci

def f(x):
    """
    This function squares its argument
    """
    return x**2

Après avoir exécuté ce code, la docstring est disponible

f?
Type:       function
String Form:<function f at 0x2223320>
File:       /home/john/temp/temp.py
Definition: f(x)
Docstring:  This function squares its argument
f??
Type:       function
String Form:<function f at 0x2223320>
File:       /home/john/temp/temp.py
Definition: f(x)
Source:
def f(x):
    """
    This function squares its argument
    """
    return x**2

Avec un point d’interrogation, nous affichons la docstring, et avec deux, nous obtenons aussi le code source.

Vous pouvez trouver les conventions pour les docstrings dans PEP257.

5.7. Exercices#

Résolvez les exercices suivants.

(Pour certains, la fonction intégrée sum() s’avère pratique).

Exercice 5.1

Partie 1 : Étant donné deux listes ou tuples numériques x_vals et y_vals de longueur égale, calculez leur produit scalaire en utilisant zip().

Partie 2 : En une seule ligne, comptez le nombre de nombres pairs dans 0,…,99.

Partie 3 : Étant donné pairs = ((2, 5), (4, 2), (9, 8), (12, 10)), comptez le nombre de paires (a, b) telles que a et b soient tous deux pairs.

Exercice 5.2

Considérez le polynôme

(5.1)#\[p(x) = a_0 + a_1 x + a_2 x^2 + \cdots a_n x^n = \sum_{i=0}^n a_i x^i\]

Écrivez une fonction p telle que p(x, coeff) calcule la valeur dans (5.1) étant donné un point x et une liste de coefficients coeff (\(a_1, a_2, \cdots a_n\)).

Essayez d’utiliser enumerate() dans votre boucle.

Exercice 5.3

Écrivez une fonction qui prend une chaîne comme argument et renvoie le nombre de lettres majuscules dans la chaîne.

Exercice 5.4

Écrivez une fonction qui prend deux séquences seq_a et seq_b comme arguments et renvoie True si chaque élément de seq_a est également un élément de seq_b, sinon False.

  • Par « séquence », nous entendons une liste, un tuple ou une chaîne.

  • Faites l’exercice sans utiliser les ensembles ni les méthodes d’ensemble.

Exercice 5.5

Lorsque nous couvrirons les bibliothèques numériques, nous verrons qu’elles incluent de nombreuses alternatives pour l’interpolation et l’approximation de fonctions.

Néanmoins, écrivons notre propre routine d’approximation de fonction comme exercice.

En particulier, sans utiliser aucun import, écrivez une fonction linapprox qui prend comme arguments

  • Une fonction f mappant un certain intervalle \([a, b]\) dans \(\mathbb R\).

  • Deux scalaires a et b fournissant les limites de cet intervalle.

  • Un entier n déterminant le nombre de points de grille.

  • Un nombre x satisfaisant a <= x <= b.

et renvoie l”interpolation linéaire par morceaux de f en x, basée sur n points de grille régulièrement espacés a = point[0] < point[1] < ... < point[n-1] = b.

Visez la clarté, pas l’efficacité.

Exercice 5.6

En utilisant la syntaxe de compréhension de liste, nous pouvons simplifier la boucle dans le code suivant.

import numpy as np

rng = np.random.default_rng()
n = 100
ϵ_values = []
for i in range(n):
    e = rng.standard_normal()
    ϵ_values.append(e)