Cours de Python

M2 Sciences des données de santé

2 Types de données intégrés

Il est indispensable de comprendre quelques concepts de base sur les types de données Python. Contrairement aux langages spécifiques au domaine tels que MATLAB ou R, où le type de données par défaut a été choisi pour l’aspect numérique, Python est un langage de programmation général qui est également bien adapté à l’analyse de données, l’économétrie et la statistique.

Par exemple, le type numérique de base de MATLAB est un tableau (utilisant la double précision, ce qui est utile pour les mathématiques en virgule flottante), alors que le type numérique de base de Python est un scalaire unidimensionnel qui peut être soit un tableau de type entier flottant de double précision, selon le formatage du nombre à l’entrée.

2.1 Noms de variables

Les noms de variables peuvent prendre de nombreuses formes, bien qu’ils ne puissent contenir que des chiffres, ils peuvent contenir des lettres (majuscules et minuscules) et des traits de soulignement (_). Ils doivent commencer par une lettre ou un trait de soulignement.

De plus, certains mots sont réservés à Python et ne peuvent donc pas être utilisés pour les noms de variables (par exemple, import ou for). Par exemple :

x = 1.0
X = 1.0
X1 = 1.0
x1 = 1.0
dell = 1.0
dellreturns = 1.0
dellReturns = 1.0
_x = 1.0
x_ = 1.0

Sont tous des noms de variables légaux distincts. Notez que les noms qui commencent ou se terminent par un trait de soulignement, bien que légaux, ne sont pas utilisés normalement car, par convention, ils ont une signification particulière.

#| eval: false
# Non autorisés 
x: = 1.0
1X = 1
X-1 = 1
for = 1

Plusieurs variables peuvent être affectées sur la même ligne à l’aide de virgules :

x, y, z = 1, 3.1415, 'a'

2.2 Types de données natifs de base

2.2.1 Numérique

Les nombres simples en Python peuvent être des entiers, des flottants ou des complexes. Ce chapitre ne couvre pas tous les types de données Python mais se concentre sur ceux qui sont les plus pertinents pour l’analyse numérique, l’économétrie et la statistique. Les types de données byte, bytearray et memoryview ne sont pas décrits.

2.2.1.1 Le type flottant

Le type de données le plus important (scalaire) pour l’analyse numérique est le type float. Malheureusement, tous les types de données numériques non complexes ne sont pas des flottants.

Pour entrer un type de données float, il est nécessaire d’inclure un . (point) dans l’expression. Cet exemple utilise la fonction type() pour déterminer le type de données d’une variable.

x = 1
print(type(x))
x = 1.0
print(type(x))
x = float(1)
print(type(x))

2.2.1.2 Nombre complexe

Les nombres complexes sont créés en Python en utilisant j ou la fonction complex()

x = 1.0
print(type(x))
x = 1j
print(type(x))
x = 2 + 3j
print(x)
x = complex(1)
print(x)

Noter que a + bj est identique à complex(a, b), alors que complex(a) est identique à a + 0j.

2.2.1.3 Entiers : Integers (int)

Les entiers de base peuvent être entrés soit en excluant la décimale (voir le type float), soit explicitement en utilisant la fonction int(). La fonction int() peut également être utilisée pour convertir un float en entier en arrondissant à 0.

x = 1
print(type(x))
x = 1.0
print(type(x))
x = int(x)
print(type(x))

La prise en charge (en mémoire) des nombres entiers Python a une plage illimitée car le nombre de bits utilisé pour stocker un nombre entier est dynamique.

x = 1
print(x)
print(type(x))
x = 2 ** 127 + 2 ** 65
print(x)
# ** denotes exponentiation, y^64 in TeX

2.2.2 Booléen (bool)

Le type de données booléen est utilisé pour représenter vrai et faux, à l’aide des mots clés réservés True et False. Les variables booléennes sont importantes pour le contrôle du flux de programme et sont généralement générées à la suite d’opérations logiques, bien qu’elles puissent être entrées directement.

x = True
print(type(x))
x = bool(1)
print(x)
x = bool(0)
print(x)

Les valeurs non nulles et non vides sont généralement considérées comme vraies lorsqu’elles sont évaluées par bool(). Les valeurs nulles ou vides telles que bool(0), bool(0.0), bool(0.0j), bool(None), bool('') et bool([]) sont toutes fausses.

2.2.3 Chaînes de caractères : Strings (str)

Les chaînes de caractères sont utilisées en traitement de fichiers de données, en particulier lors de l’importation ou du formatage d’une sortie. Les chaînes de caractères sont délimitées par des guillemets simples ('') ou des doubles guillemets (""), mais pas par la combinaison des deux délimiteurs.

x = 'abc'
print(type(x))
y = '"A quotation!"'
print(y)

2.2.3.1 Manipulation des chaînes de caractères

Les sous-chaînes d’une chaîne sont accessibles via l’opération dite de slicing (découpage en tranches). Le découpage en tranches utilise [] pour contenir les indices des caractères d’une chaîne, où le premier indice est 0 et le dernier est n-1 (en supposant que la chaîne de caractères a n lettres).

Le tableau ci-dessous fournit une liste des types de slicing pouvant être utilisés. La deuxième colonne montre que le slicing peut utiliser des indices négatifs qui indexent essentiellement la chaîne en arrière.

Slice Signification
s[:] La chaîne de caractères en entier
s[i] Le caractère à la position i
s[i:] Caractères allant de i à n-1
s[:i] Caractères allant de 0 à i-1
s[i:j] Caractères allant de i à j-1
s[i:j:m] Caractères allant de i à j-1par un pas de m
s[−i] Caractère à la position n-i
s[−i:] Caractères allant de n-i à n-1
s[:−i] Caractères allant de 0 à n-i-1
s[−j:−i] Caractères allant de n-j à n-i-1 avec -j<-i
s[−j:−i:m] Caractères allant de n-j à n-i-1 par un pas de m
#| error: true
text = 'Python strings are sliceable.'
print(text[0])
print(text[10])
L = len(text)
print(text[L]) # Error: IndexError
print(text[L-1])
print(text[:10])
print(text[10:])

2.2.4 Les listes

Les listes sont un type de données conteneur intégré qui contient d’autres données. Une liste est une collection d’autres objets : flottants, entiers, nombres complexes, chaînes de caractères ou même d’autres listes.

Les listes sont essentielles à la programmation Python et sont utilisées pour stocker des collections d’autres valeurs. Par exemple, une liste de flottants peut être utilisée pour représenter un vecteur (bien que les types array et matrix de NumPy soient mieux adaptés).

Les listes de base sont construites à l’aide d’accolades carrées, [], et les valeurs sont séparées par des virgules.

x = []
print(type(x))
x=[1,2,3,4]
print(x)
# 2-dimensional list (list of lists)
x = [[1,2,3,4], [5,6,7,8]]
print(x)
# Jagged list, not rectangular
x = [[1,2,3,4] , [5,6,7]]
print(x)
# Mixed data types
x = [1,1.0,1+0j,'one',None,True]
print(x)

Ces exemples montrent que les listes peuvent être régulières, imbriquées et peuvent contenir n’importe quel mélange de types de données, y compris d’autres listes.

2.2.4.1 Manipuler les listes

Les listes, comme les chaînes de caractères, peuvent être manipulées. L’opération de slicing est similaire. Les listes peuvent être multidimensionnelles alors que les chaînes de caractères sont toujours de dimension \(1 \times n\). L’opération de slicing de listes de base est identique à celui des chaînes de caractères, et des opérations telles que x[:], x[1:], x[:1] et x[-3:] peuvent toutes être utilisées.

La manipulation d’une liste par défaut utilise une unité de pas (taille d’un pas). Il est possible d’utiliser d’autres pas en utilisant un troisième entrée dans l’instruction x[i:j:m] où i est l’indice de début, j est l’indice de fin (exclusif) et m est la longueur du pas.

Par exemple, x[::2] sélectionnera un élément sur deux d’une liste. Le pas peut également être négatif, ce qui peut être utilisé pour sélectionner éléments d’une liste dans l’ordre inverse (x[::-1]).

#| error: true
x = [0,1,2,3,4,5,6,7,8,9]
print(x[0])
print(x[5])
print(x[10]) # Error: IndexError
print(x[4:])
print(x[:4])
print(x[1:4])
print(x[-0])
print(x[-1])
print(x[-10:-1])

La liste peut être multidimensionnelle et le slicing peut être effectué directement dans des dimensions supérieures.

Pour simplifier, soit une liste en 2 dimensions x=[[1,2,3,4], [5,6,7,8]]. Si l’indexation simple est utilisée, x[0] renverra la première liste (interne) et x[1] renverra la deuxième liste (interne). La liste interne peut être manipulée directement à l’aide de x[0][0] ou x[0][1:4].

x = [[1,2,3,4], [5,6,7,8]]
print(x[0])
print(x[1])
print(x[0][0])
print(x[0][1:4])
print(x[1][-4:-1])

2.2.4.2 Fonctions associées à un objet list

Un certain nombre de fonctions sont disponibles pour manipuler des listes. Les plus utiles sont :

x = [0,1,2,3,4,5,6,7,8,9]
x.append(0)
print(x)
print(len(x))
x.extend([11,12,13])
print(x)
print(x.pop(1))
print(x)
x.remove(0)
print(x)

Les éléments peuvent également être supprimés des listes en utilisant le mot-clé del en combinaison avec un slice.

x = [0,1,2,3,4,5,6,7,8,9]
del x[0]
print(x)
print(x[:3])
del x[:3]
print(x)
del x[1:3]
print(x)
del x[:]
print(x)

2.2.5 Tuples (tuple) ou n-uplet

Un tuple est pratiquement identique à une liste avec une différence importante: les tuples sont immuables. L’immuabilité signifie qu’un tuple ne peut pas être changé une fois créé.

Il n’est pas possible d’ajouter, de supprimer ou de remplacer des éléments dans un tuple. Toutefois, si un tuple contient un type de données mutable, par exemple un tuple contenant une liste, le contenu peut être modifié.

Les tuples sont construits en utilisant des parenthèses (()) à la place des crochets ([]) utilisés pour créer des listes. Les tuples peuvent être manipulés de la même manière que les listes.

x =(0,1,2,3,4,5,6,7,8,9)
print(type(x))
print(x[0])
print(x[-10:-5])
x = list(x)
print(type(x))
x = tuple(x)
print(type(x))
x = ([1,2],[3,4])
x[0][1] = -10
print(x) # Contents can change, elements cannot

Noter que les tuples contenant un seul élément doivent contenir une virgule lors de leur création, de sorte que l’instruction x = (2,) revient assigner un tuple à x, alors que x=(2) assignera 2 à x.

x =(2)
print(type(x))
x = (2,)
print(type(x))
x = tuple([2])
print(type(x))

2.2.5.1 Fonctions associées aux tuples

Les tuples sont immuables et n’ont donc que les méthodes index et count, qui fonctionnent de la même manière que le type list.

2.2.6 Le type dictionary (dict)

Les dictionnaires sont couramment utilisés pour transmettre des options à d’autres fonctions telles que les optimiseurs. Les dictionnaires en Python sont composés de clés (mots) et valeurs (définitions). Les clés doivent être des types de données immuables uniques, et les valeurs peuvent contenir tout type de données Python valide.

data = {'age': 34, 'children' : [1,2], 1: 'apple'}
print(type(data))
print(data['age'])

Les valeurs associées à une clé existante peuvent être mises à jour en attribuant une affectation à la clé dans le dictionnaire.

data['age'] = 'xyz'
print(data['age'])

De nouvelles paires clé-valeur peuvent être ajoutées en définissant une nouvelle clé et en lui attribuant une valeur.

data['name'] = 'abc'
print(data)

Les paires clé-valeur peuvent être supprimées à l’aide du mot clé réservé del.

del data['age']
print(data)

2.2.7 Le type Set (set, frozenset)

Un set est une collection d’éléments uniques. set et frozenset ne diffèrent que par le fait que ce dernier est immuable.

2.2.7.1 Les méthodes associées au type set

x = set(['MSFT','GOOG','AAPL','HPQ','MSFT'])
print(x)
x.add('CSCO')
print(x)
y = set(['XOM', 'GOOG'])
print(x.intersection(y))
print(x.difference(y))
x = x.union(y)
print(x)
x.remove('XOM')
print(x)

Le type frozenset supporte les mêmes méthodes sauf add et remove.

2.2.8 Les séquences : le type range

Le type range est souvent rencontré dans une boucle for. range(a, b, i) crée la séquence \(a, a + i, a + 2i,\ldots , a + (m - 1)i\) où deux valeurs consécutives sont séparées par \(i\).

x = range(10)
print(type(x))
print(x)
print(list(x))

x = range(3,10)
print(list(x))

x = range(3,10,3)
print(list(x))

Le type range n’est pas techniquement une liste, c’est pourquoi l’instruction print(x) affiche range(0,10). La conversion explicite avec la liste produit une liste qui permet d’afficher les valeurs. Techniquement, range est un itérateur qui ne nécessite pas réellement l’espace de stockage d’une liste.

2.3 Python et la gestion de la mémoire

Python utilise un système d’allocation de mémoire hautement optimisé pour éviter l’allocation de mémoire inutile. Par conséquent, lorsqu’une variable est affectée à une autre (par exemple, y = x), celles-ci désignent les mêmes données dans la mémoire de l’ordinateur. Pour vérifier cela, id() peut être utilisé.

x = 1
y = x
print(id(x))
print(id(y))

x = 2.0
print(id(x))
print(id(y))

Sur cet exemple, l’affectation initiale y=x a généré deux variables avec le même ID. Cependant, une fois que x a été modifié, son identifiant a changé, mais pas l’identifiant de y, indiquant que les données de chaque variable ont été stockées dans des emplacements différents. Ce comportement est à la fois sûr et efficace.

2.3.1 Exemple des listes

Les listes étant modifiables, les affectations ne créent pas de copie et les modifications apportées à l’une ou l’autre des variables affectent les deux.

x = [1, 2, 3]
y = x
y[0] = -10
print(y)
print(x)

L’opération de slicing d’une liste crée une copie de la liste et de tous les types immuables de la liste.

x = [1, 2, 3]
y = x[:]

print(id(x))
print(id(y))

x=[[0,1],[2,3]]
y = x[:]

print(y)
print(id(x[0]))
print(id(y[0]))

print(x[0][0])
print(id(x[0][0]))
print(id(y[0][0]))

y[0][0] = -10.0

print(y)
print(x)

On remarque que l’opération de slicing attribue un nouvel ID mais les éléments internes restent liés. Pour copier les listes imbriquées, le module copy est indispensable.

import copy as cp
x=[[0,1],[2,3]]
y = cp.deepcopy(x)
y[0][0] = -10.0
print(y)
print(x)

Un peu d’entraînement

Initialiser une liste contenant 4, 3.1415, 1.0, 2+4j, 'Hello', 'World'.

  • Supprimer 1.0 si sa position est connue. Si sa position est inconnue, que doit-on faire ?
  • Comment la liste [1.0, 2 + 4j, 'Hello'] peut-elle être ajoutée à la liste existante ?
  • Comment peut-on inverser la liste ?
  • Dans la liste étendue, comment peut-on compter l’occurrence de 'Hello' ?
l = [4, 3.1415, 1.0, 2+4j, 'Hello', 'World']

5. Contrôle de flux et boucles

Nous avons précédemment fait appel aux variables logiques, en sélectionnant des éléments dans un tableau. Le contrôle de flux utilise également des variables logiques pour permettre à différents codes d’être exécutés selon que certaines conditions sont remplies ou non. Le contrôle de flux en Python se présente sous deux formes: instruction conditionnelle et les boucles.

5.1 Contrôle des espaces et des flux

Python utilise des modifications d’espace pour indiquer le début et la fin des blocs de contrôle de flux, l’indention est donc importante. Par exemple, lors de l’utilisation des blocs if. . . elif. . . else, tous les blocs de contrôle doivent avoir le même niveau d’indentation et toutes les instructions à l’intérieur des blocs de contrôle doivent avoir le même niveau d’indentation.

Revenir au niveau d’indentation précédent indique à Python que le bloc est terminé. La meilleure pratique consiste à utiliser uniquement des espaces (et non des tabulations) et 4 espaces lors du démarrage d’un nouveau niveau d’indentation. Ce qui représente un équilibre raisonnable entre lisibilité et gaspillage d’espace.

5.2 if ... elif ... else

Les blocs if ... elif ... else commencent toujours par une instruction if immédiatement suivie d’une expression logique scalaire. elif et else sont facultatifs et peuvent toujours être replacés par des instructions if imbriquées aux dépens d’une logique plus complexe et d’une imbrication plus profonde.

La forme générique d’un bloc if ... elif ... else est donnée par :

#| eval: false
if logical_1:
   Code to run if logical_1    
elif logical_2:
    Code to run if logical_2 and not logical_1
...
else:
    Code to run if all previous logicals are false    

Un petit exemple :

x = 5
if x<5:
    x = x + 1
elif x>5:
    x = x - 1
else:
    x = x * 2

print(x)

5.3 for

Les boucles for commencent par for item dans iterable:, et la structure générique d’une boucle for est :

#| eval: false
for item in iterable:
     Code to run

item est un élément d’un objet itérable de Python. Les exemples les plus courants sont les range, les listes, les tuples, les tableaux ou les matrices. La boucle for parcourt tous les éléments de l’objet itérable, en commençant par l’élément \(0\) et se poursuivant jusqu’à l’élément final.

Lors de l’utilisation de tableaux multidimensionnels, seule la dimension extérieure est directement itérable. Par exemple, si \(x\) est un tableau à \(2\) dimensions, les éléments itérables sont \(x[0]\), \(x[1]\) et ainsi de suite.

import numpy as np
count = 0
for i in range(100):
    count += i
print(count)

count = 0
x = np.linspace(0,500,50)
for i in x:
    count += i    
print(x)

count = 0
x = list(np.arange(-20,21))
for i in x:
    count += i    
print(x)

La première boucle itérera sur \(i = 0, 1, 2, \ldots,99\). La seconde boucle sur les valeurs produites par la fonction linspace, qui renvoie un tableau avec \(50\) points uniformément espacés compris entre \(0\) et \(500\) inclus. La dernière boucle sur \(x\), un vecteur construit à partir d’un appel à liste(arange (-20,21)), qui produit une liste contenant la séquence \(-20, -19, \ldots, 0, \ldots, 19,20\).

La clé pour comprendre le comportement en boucle est le suivant : il effectue toujours une itération sur les éléments de l’iterable dans l’ordre dans lequel ils sont présentés (c’est-à-dire iterable[0], iterable[1],…).

Les boucles peuvent aussi être imbriquées :

count = 0
for i in range(10):
    for j in range(10):
        count += j

Ou peuvent contenir des variables de contrôle de flux :

from numpy import random
returns = random.randn(100)
count = 0
for ret in returns:
    if ret<0:
        count += 1

Cette boucle for peut être exprimée de manière équivalente en utilisant range en tant qu’itérateur et len pour obtenir le nombre d’éléments contenus dans l’objet itérable.

import numpy as np
returns = np.random.randn(100)
count = 0
for i in range(len(returns)):
    if returns[i]<0:
        count += 1
print(count)

Enfin, ces idées peuvent être combinées pour produire des boucles imbriquées avec contrôle de flux.

import numpy as np
x = np.zeros((10,10))
for i in range(np.size(x,0)):
    for j in range(np.size(x,1)):
        if i<j:
            x[i,j]=i+j
        else:
            x[i,j]=i-j
print(x)

Ou des boucles contenant des boucles imbriquées exécutées en fonction d’une instruction de contrôle de flux.

x = np.zeros((10,10))
for i in range(np.size(x,0)):
    if (i % 2) == 1:
        for j in range(np.size(x,1)):
            x[i,j] = i+j
    else:
        for j in range(int(i/2)):
            x[i,j] = i-j
print(x)

Important : La variable itérable ne doit pas être réaffectée une fois dans la boucle. Considérons, par exemple :

x = range(10)
for i in x:
    print(i)
    print('Length of x:', len(x))
    x = range(5)

Il n’est pas prudent de modifier l’objet itérable lorsqu’on boucle dessus. Cela signifie que l’itérable ne doit pas changer de taille, ce qui peut se produire lors de l’utilisation d’une liste et des fonctions pop(), insert() ou append() ou du mot clé del.

La boucle ci-dessous ne se terminera jamais (sauf pour l’instruction if qui coupe la boucle) puisque \(L\) est étendu à chaque itération.

#| eval: false
L = [1, 2]
for i in L:
    print(i)
    L.append(i+2)
    if i>5:
        break

Enfin, les boucles for peuvent être utilisées avec \(2\) itérables encapsulés dans enumerate, ce qui permet d’accéder directement aux éléments de l’itérable, ainsi que leur indice dans l’itéré.

import numpy as np
x = np.linspace(0,100,11)
for i,y in enumerate(x):
    print('i is :', i)
    print('y is :', y)

5.3.1 Les espaces

Comme les blocs de contrôle de flux if...elif... else, les boucles for sont sensibles aux espaces. L’indentation de la ligne immédiatement en dessous de l’instruction for détermine l’indentation que doivent avoir toutes les instructions du bloc.

5.3.2 break

Une boucle peut être terminée tôt en utilisant break. break est généralement utilisé après une instruction if pour mettre fin à la boucle prématurément si une condition est remplie.

#| eval: false
from numpy.random import randn
x = randn(1000)
for i in x:
    print(i)
    if i > 2:
        break

Etant donné que les boucles for parcourent une valeur itérable de taille fixe, break est généralement plus utile dans les boucles while.

5.3.3 continue

continue peut être utilisé pour ignorer une itération d’une boucle et revenir immédiatement au début de la boucle en utilisant l’élément suivant dans iterable. continue est couramment utilisé pour éviter un niveau d’imbrication, comme dans les deux exemples suivants :

#| eval: false
x = randn(10)
for i in x:
    if i < 0:
        print(i)
        
for i in x:
    if i >= 0:
        continue
    print(i)

Éviter les niveaux excessifs d’indentation est essentiel dans la programmation Python. 4 espaces est généralement considéré comme le niveau maximum raisonnable. continue est particulièrement utile dans la mesure où il peut être utilisé dans une boucle for pour éviter un creux d’indentation.

5.4 while

Les boucles while sont utiles lorsque le nombre d’itérations nécessaires dépend du résultat du contenu de la boucle. Lorsqu’une boucle ne doit s’arrêter que si certaines conditions sont remplies, par exemple lorsque la modification de certains paramètres est faible. La structure générique d’une boucle while est :

#| eval: false
while logical:
    Code to run
    Update logical

Deux choses sont importantes lors de l’utilisation d’une boucle while: premièrement, l’expression logical doit être évaluée à vraie lorsque la boucle commence (ou la boucle sera ignorée) et deuxièmement, les entrées de l’expression logical doivent être mises à jour dans la boucle. Si ce n’est pas le cas, la boucle continuera indéfiniment (appuyez sur CTRL + C pour interrompre une boucle interminable dans IPython).

count = 0
i = 1
while i<10:
    count += i
    i += 1

Qui produit le même résultat que :

count=0
for i in range(0,10):
    count += i

Les boucles while doivent généralement être évitées lorsque les boucles for suffisent. Cependant, il existe des situations où aucun équivalent de boucle for n’existe.

import numpy as np
# randn generates a standard normal random number
mu = abs(100*np.random.randn(1))
index = 1
while abs(mu) > .0001:
    mu = (mu+np.random.randn(1))/index
    index=index+1

Dans le bloc ci-dessus, le nombre d’itérations requis n’est pas connu à l’avance et puisque randn est la réalisation d’une variable aléatoire normale, il peut prendre de nombreuses itérations jusqu’à ce que ce critère soit rempli.

5.4.1 break

break peut être utilisé dans une boucle while pour terminer immédiatement l’exécution. Normalement, break ne doit pas être utilisé dans une boucle while. Toutefois, break peut être utilisé pour éviter d’exécuter du code en dessous de l’instruction break, même si la condition logique est False.

import numpy as np
condition = True
i = 0
x = np.random.randn(1000000)
while condition:
    if x[i] > 3.0:
        break # No printing if x[i] > 3
        
    print(x[i])
    i += 1

Il est préférable de mettre à jour l’instruction logique qui détermine si la boucle while doit être exécutée.

# Suite du code précédent
i = 0
while x[i] <= 3 and i < len(x):
    print(x[i])
    i += 1

5.4.2 continue

continue peut être utilisé dans une boucle while pour ignorer tout code restant dans la boucle et revenir immédiatement au début de la boucle, qui vérifie ensuite la condition while et exécute la boucle si elle est toujours vraie. Utiliser continue lorsque la condition logique dans la boucle while est False est identique à utiliser break.

5.5 try ... except

La gestion des exceptions est une technique de programmation avancée qui peut être utilisée pour produire un code plus résistant (souvent au détriment de la vitesse). Les blocs de code try ... except sont utiles pour exécuter du code qui peut échouer pour des raisons hors du contrôle du programmeur.

Dans la plupart des applications numériques, le code doit être déterministe et le code qui échoue peut généralement être évité. Par exemple, si vous ne pouvez pas lire les données d’une source de données qui n’est pas toujours disponible (par exemple, un site Web), try ... except peut être utilisé pour tenter d’exécuter le code, puis pour faire quelque chose si le code ne parvient pas à s’exécuter.

La structure générique d’un bloc try ... except est :

#| eval: false
try:
    Dangerous Code
except ExceptionType1:
    Code to run if ExceptionType1 is raised
except ExceptionType2:
    Code to run if ExceptionType1 is raised
except:
    Code to run if an unlisted exception type is raised

Un exemple simple de gestion des exceptions se produit lors de la tentative de conversion de texte en chiffres.

text = ('a','1','54.1','43.a')
for t in text:
    try:
        temp = float(t)
        print(temp)
    except ValueError:
        print('Not convertable to a float')

5.6 Compréhension des listes

La compréhension de liste est une méthode optimisée pour créer une liste qui peut simplifier le code lorsqu’un objet iterable est bouclé et que les résultats sont sauvegardés dans une liste, éventuellement conditionnée par un test logique. Une simple liste peut être utilisée pour convertir une boucle for qui inclut un ajout en une seule ligne.

import numpy as np
x = np.arange(5.0)
y = []
for i in range(len(x)):
    y.append(np.exp(x[i]))

print(y)

z = [np.exp(x[i]) for i in range(len(x))]
print(z)

On peut inclure une condition if :

from math import floor
x = np.arange(5.0)
y = []
for i in range(len(x)):
    if i % 2 == 0 :
        y.append(x[i]**2)
print(y)

z = [x[i]**2 for i in range(len(x)) if floor(i/2)==i/2]
print(z)

On peut aussi le faire avec des doubles boucles :

x1 = np.arange(5.0)
x2 = np.arange(3.0)
y = []
for i in range(len(x1)):
    for j in range(len(x2)):
        y.append(x1[i]*x2[j])
print(y)

z = [x1[i]*x2[j] for i in range(len(x1)) for j in range(len(x2))]
print(z)

# Only when i==j
v = [x1[i]*x2[j] for i in range(len(x1)) for j in range(len(x2)) if i==j]
print(v)

5.7 Compréhension des dictionnaires, tuples et ensembles

import numpy as np
from math import exp
x = np.arange(-5.0,5.0)
z_set = {x[i]**2.0 for i in range(len(x))}
print(z_set)

z_dict = {i:exp(i) for i in x}
print(z_dict)

z_tuple = tuple(i**3 for i in x)
print(z_tuple)

5.8 Un peu d’entraînement

5.8.1

Trouver deux méthodes différentes pour utiliser une boucle for afin de remplir un tableau \(5 \times 5\) avec \(i \times j\), où \(i\) est l’indice de la ligne et \(j\) est l’indice de la colonne. L’une utilisera range comme itérable, et l’autre devrait directement itérer sur les lignes, puis les colonnes de la matrice.

5.8.2

Écrire une compréhension de liste qui itérera sur un tableau à une dimension et extraire les éléments négatifs dans une liste. Comment cela peut-il être fait en utilisant seulement des fonctions logiques (pas de boucle explicite), sans la compréhension de la liste (et en retournant un tableau) ?

import numpy as np

x = np.random.randn(10000000)
# %time y1 = [v for v in x if v < 0]
# %time y2 = x[x<0]

6. Les fonctions

Python prend en charge un large éventail de styles de programmation, notamment procédural (impératif), orienté objet et fonctionnel. Bien que la programmation orientée objet et la programmation fonctionnelle soient de puissants paradigmes de programmation, en particulier dans les logiciels volumineux et complexes, la procédure est souvent à la fois plus facile à comprendre et à représenter directement une formule mathématique.

L’idée de base de la programmation procédurale est de produire une fonction ou un ensemble de fonctions (génériquement) de la forme :

\[y = f(x)\]

Les fonctions prennent une ou plusieurs entrées et produisent une ou plusieurs sorties.

Les fonctions Python sont très simples à déclarer et peuvent se trouver dans le même fichier que le programme principal ou dans un programme standard. Les fonctions sont déclarées à l’aide du mot clé def et la valeur produite est renvoyée à l’aide du mot clé return.

Considérons une fonction simple qui renvoie le carré de l’entrée, \(y = x^2\).

def square(x):
    return x**2

# appel de la fonction
x = 2
y = square(x)
print(x,y)

Un autre exemple :

def l2distance(x,y):
    return (x-y)**2

# appel de la fonction
x = 3
y = 10
z = l2distance(x,y)
print(x,y,z)

La fonction peut également être définie à l’aide de tableaux et de matrices NumPy.

import numpy as np

def l2_norm(x,y):
    d = x - y
    return np.sqrt(d @ d)

# appel de la fonction
x = np.random.randn(10)
y = np.random.randn(10)
z = l2_norm(x,y)

print(x-y)
print("La distance L2 ",z)

Lorsque plusieurs sorties sont renvoyées mais qu’une seule variable est disponible pour l’affectation, toutes les sorties sont renvoyées dans un tuple. Alternativement, les sorties peuvent être directement affectées lorsque la fonction est appelée avec le même nombre de variables que les sorties.

import numpy as np

def l1_l2_norm(x,y):
    d = x - y
    return sum(np.abs(d)),np.sqrt(d @ d)
# appels de la fonction
x = np.random.randn(10)
y = np.random.randn(10)

# Avec un appel à un seul objet de sortie
z = l1_l2_norm(x,y)

print(x-y)
print("La distance L1 est ",z[0])
print("La distance L2 est ",z[1])

# Avec un appel avec 2 objets de sortie  
l1,l2 = l1_l2_norm(x,y)
print("La distance L1 est ",l1)
print("La distance L2 est ",l2)
print(type(z))

Il est possible d’ignorer certaines sorties avec le tiret bas _ :

_,h = l1_l2_norm(x,y)
print(h)

6.1 Arguments mots clés et valeurs par défaut

Toutes les variables d’entrée dans les fonctions sont automatiquement des arguments mot-clés. On peut ainsi accéder à la fonction en plaçant les entrées dans l’ordre dans lequel elles apparaissent dans la fonction ou en appelant l’entrée par leur nom à l’aide de mot-clé = valeur.

import numpy as np

def lp_norm(x,y,p):
    d = x - y
    return sum(abs(d)**p)**(1/p)

# appels de la fonction
x = np.random.randn(10)
y = np.random.randn(10)
z1 = lp_norm(x,y,2)
z2 = lp_norm(p=2,x=x,y=y)

print("Les distances Lp sont ",z1,z2)

Comme les noms de variable sont automatiquement des mots-clés, il est important d’utiliser des noms de variable significatifs lorsque cela est possible, plutôt que des variables génériques telles que \(a, b, c\) ou \(x, y\) et \(z\).

6.1.1 Valeurs par défaut

Les valeurs par défaut sont définies dans la déclaration de fonction à l’aide de la syntaxe input = default.

import numpy as np

def lp_norm(x,y,p = 2):
    d = x - y
    return sum(abs(d)**p)**(1/p)

# appel de la fonction
x = np.random.randn(10)
y = np.random.randn(10)

# Les arguments de la fonction (les valeurs par défaut peuvent être ignorées)
l2 = lp_norm(x,y)
l1 = lp_norm(x,y,1)
print("Les distances l1 et l2 sont ",l1,l2)
print("Vérification ", sum(abs(x-y))==l1)

Quelques bonnes pratiques avec les valeurs par défaut :

import numpy as np

def bad_function(x = np.zeros(1)):
    print(x)
    x[0] = np.random.randn(1)

# appels
bad_function()
bad_function()
bad_function()

Chaque appel à bad_function indique que \(x\) a une valeur différente bien que la valeur par défaut soit \(0\).

La solution à ce problème consiste à initialiser les objets mutables sur None, puis à utiliser un if pour vérifier et initialiser uniquement si la valeur est None. Noter que les tests pour None utilisent le mot-clé is plutôt que le test d’égalité avec ==.

import numpy as np

def good_function(x = None):
    if x is None:
        x = np.zeros(1)
    print(x)
    x[0] = np.random.randn(1)
    
# appels
good_function()
good_function()

6.1.2 Un nombre variable d’entrées

En utilisant *args :

import numpy as np

def lp_norm(x,y,p = 2, *args):
    d = x - y
    print('The L' + str(p) + ' distance is :', sum(abs(d)**p)**(1/p))
    out = [sum(abs(d)**p)**(1/p)]
    print('Number of *args:', len(args))
    for p in args:
        print('The L' + str(p) + ' distance is :', sum(abs(d)**p)**(1/p))
        out.append(sum(abs(d)**p)**(1/p))
    return tuple(out)

# appels de la fonction
x = np.random.randn(10)
y = np.random.randn(10)

# Avec des arguments optionnels
lp = lp_norm(x,y,1,2,3,4,1.5,2.5,0.5)

En utilisant **kwargs :

import numpy as np

def lp_norm(x,y,p = 2, **kwargs):
    d = x - y
    print('The L' + str(p) + ' distance is :', sum(abs(d)**p)**(1/p))
    out = [sum(abs(d)**p)**(1/p)]
    print('Number of *kwargs:', len(kwargs))
    for key in kwargs:
        print('Key :', key, ' Value:', kwargs[key])
        out.append(sum(abs(d)**kwargs[key])**(1/kwargs[key]))
    return  out

x = np.random.randn(10)
y = np.random.randn(10)

# L'argument p est dans la définition de la fonction donc n'est pas considéré comme **kwargs
lp = lp_norm(x,y,toto1=1,toto2=3.2,p=0)
print(lp)

6.2 La chaîne de documentation

La chaîne de caractères de documentation est l’un des éléments les plus importants de toute fonction - en particulier une fonction écrite pour être utilisée par d’autres. La chaîne de documentation est une chaîne spéciale, entourée de guillemets triples, soit ''', soit """, disponible à l’aide de help().

Lorsque help(fun) est appelé, Python cherche la chaîne de caractères de documentation qui est placée juste en dessous de la définition de la fonction.

import numpy as np

def lp_norm(x,y,p = 2):
    """ Le  docstring contient l'aide de la fonction.
        Un bon docstring doit expliquer les entrées et sorties, 
        et fournit un exemple.
    """
    d = x - y
    return sum(abs(d)**p)

help(lp_norm)

Cette chaîne de caractère de documentation n’est pas un bon exemple. Il est suggéré de suivre les instructions de NumPy, qui sont plus appropriées pour le code numérique. Une meilleure chaîne de caractères de documentation pour lp_norm serait :

import numpy as np
def lp_norm(x,y,p = 2):
    """ Calcul de distance entre vecteurs.
    
    La distance associée à la norme Lp est sum(abs(x-y)**p)**(1/p)
    
    Paramètres
    ----------
    x : ndarray
        Premier argument
    y : ndarray
        Second argument
    p : float, optionnel
        Puissance utilisée pour le calcul de la distance >=0
        
    Sorties
    -------
    sortie : scalaire
        Retourne la distance associée à la norme Lp entre x et y
    
    Notes
    -----
    Pour p>=1, la sortie est décrite précédemment. 
    Pour 0<=p<1, la sortie est sum(abs(x-y)**p).
    
    Exemples
    --------
    >>> x=[0,1,2]
    >>> y=[1,2,3]
    >>> lp_norm(x,y)
    """
    if p<0: p=0
    d = x - y

    if p == 0:
        return sum(d != 0)
    elif p < 1:
        return sum(abs(d)**p)
    else:
        return sum(abs(d)**p)**(1/p)
    
help(lp_norm)