Python pour les scientifiques

Une rapide introduction

Jeremy Fix

CentraleSupélec

Introduction

Syllabus

Module SPM-INF-002:

Evaluation par le rendus du TP5, limite 1 semaine après le TP : code sur https://gitlab-student.centralesupelec.fr + Rapport avec illustrations du travail.

Ressources

Scientific python lectures aborde :

  • le langage python
  • Numpy, Matplotlib, Scipy,
  • des sujets avancés (interface python/C++)

Scientific python lectures

Petit tour d’horizon

L’écosystème python est très riche en librairies scientifiques :

  • Numpy pour le calcul,
  • Pandas pour l’analyse de données,
  • Scipy pour le traitement du signal,
  • Scikit-learn pour l’apprentissage automatique,
  • Matplotlib/Seaborn pour les tracés,
  • Sympy pour le calcul symbolique,

Introduction à l’interpréteur python

Les interpréteurs

Python est un langage interprété

  • Langage compilé = déjà transformé en instructions CPU
  • Langage interprété = un programme (compilé :) ) transforme “à la volée” votre code en instructions.

“à la volée”: cpython peut créer un fichier .pyc qui contient le code compilé (bytecode).

fix_jer@stollen:~$ python3
Python 3.8.10 (default, May 26 2023, 14:05:08) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> l = range(100)
>>> sum(l)
4950
>>> 

Les interpréteurs

Instructions CPU x86, arm.

Performances de différentes version du “même” code

Un code lent et gourmand en mémoire

l = list(range(10000000))
lsum = 0
for i in range(len(l)): 
    lsum += l[i]

Mauvais code

  • range explicité en mémoire \(\neq\) paresseuse
  • boucle for en python
  • l’accès indicé l[i] pourrait être problématique mais les listes python sont des tableaux et pas des listes chaînées

Un peu plus rapide, moins gourmand en mémoire

l = range(10000000)
lsum = 0
for li in l: 
    lsum += li

Mauvais code

  • la boucle for est exécutée en python, c’est à dire interprétée

Le “même” code mais plus efficace

Bien plus vite tout en restant paresseux en mémoire

l = range(10000000)
lsum = sum(l)

Fondamentalement différent

  • Appel de la fonction built-in sum, déjà compilée

Plus rapide mais plus gourmand avec numpy

import numpy as np

l = np.arange(10000000)
lsum = l.sum()

Attention donc au “c’est le même code”, “mais ça marche”.

Un autre bénéfice de la compilation : le code vectorisé

Instructions vectorielles

  • les processeurs modernes disposent d’instructions vectorielles, e.g. AVX
  • une instruction vectorielle (SIMD : Single Instruction Multiple Data) peut appliquer la même opération sur plusieurs données (e.g. \(4\) données)
  • l’interpréteur python CPython ne supporte pas les instructions vectorisées,
  • les compilateurs, e.g. pour C++ comme gcc ou clang, réorganisent le code pour exploiter les instructions SIMD
  • les outils/librairies comme numpy, cython ou pypy compilent du code python et peuvent utiliser les instructions SIMD

Installation des packages

Environnement virtuel

Un environnement virtuel est :

  • un répertoire isolé d’installation
  • qui a vocation à ne contenir que les dépendances de votre projet en cours
  • dont la construction est documentée

Il permet la reproductibilité (Benureau and Rougier 2018), avec le contrôle des packages installés et de leurs versions.

Différentes options :

  • venv (inclus dans Python depuis la version 3.3) : python package manager, ne permet pas de changer de version de python
  • anaconda, miniconda, mamba, micromamba, etc… : system package manager. Permet de changer de version de python, installer des dépendances autres que des packages python (e.g. C++, Java, Rust, …), mais parfois un peu lourd
  • uv https://docs.astral.sh/uv : python package manager, flexible sur la version de python, ne permet d’installer que des paquets python

Exemple avec micromamba

Pour installer micromamba (en utilisateur normal):

$ "${SHELL}" <(curl -L micro.mamba.pm/install.sh)

Pour construire un environnement virtual local dédié :

$ micromamba create -p ./mon_env python=3.13
$ micromamba activate ./mon_env

Pour installer des dépendances :

$ micromamba install numpy pandas matplotlib
$ micromamba install numpy=2.0.1

On peut exporter, recharger, etc.. un environnement.

Exemple avec uv

Pour installer uv (en utilisateur normal) :

$ curl -LsSf https://astral.sh/uv/install.sh | sh

Pour construire un environnement virtuel local dédié :

$ uv venv --python 3.13 mon_venv
$ source mon_venv/bin/activate

Pour installer des dépendances :

$ uv pip install numpy pandas matplotlib
$ uv pip install numpy==2.0.1
$ uv pip install -r requirements.txt
$ uv pip install . # pour un projet avec un pyproject.toml par exemple

Environnement de développement

VS Code

Warning

Vous devez utiliser un Integrated Development Environment (IDE)

Suggestion : utilisez Visual Studio Code + tout son environnement de plugins qui permet :

  • de gérer votre projet (création, organisation des fichiers)
  • disposer de la coloration syntaxique, complétion de code, etc…
  • mettre en place un environnement virtuel et exécuter le code dans cet environnement
  • utiliser des notebooks jupyter
  • interagir avec git

VS Code https://code.visualstudio.com/docs/languages/python

=> Démo avec Venv

Jupyter notebook/lab

On peut également travailler avec des notebooks jupyter.

$ uv pip install jupyter
$ jupyter notebook

Fonctionnalités :

  • supporte des noyaux python, c++, R, …
  • permet de combiner du texte et du rendu
  • permet d’inclure des widgets dynamiques (e.g. sliders)

Voir ces exemples à essayer dans le navigateur, e.g. le simulateur de l’attracteur de Lorentz

Notebook jupyter, démo Lorenz.ipynb

Notebook jupyter, démo Lorenz.ipynb

Attention à l’ordre d’exécution des cellules.

=> Démo avec venv pour l’installer localement

Python : bases

Rappels de syntaxe de base

  • ATTENTION à l’indentation
  • types int, float, bool, set
  • types “séquence” immutables: str, tuple
  • type “séquence” mutable : list
  • type séquence clé/valeur : dict
  • interroger le type type(x), tester le type isinstance(x, letype), connaître les méthodes applicables dir(x)
  • opérateur d’affectation x = 3, walrus
with open(fname, 'r') as fh:
    while((line:=fh.readline()) != ''):
        print(line.rstrip())
  • opérateurs arithmétiques (+, -, /, *), et logiques (and, or, not)
  • branchements conditionnés if ... else .. if ... elif ... else
  • opérateur ternaire, lambda fonction
syr = lambda x: x//2 if x%2 == 0 else 2*x+1

x = 18 
syr(x)
  • boucles for x in [1, 2, 3]:, while condition:

Rappels de syntaxe de base

Sur les listes

l = [1, 2, 3]

# Listes par compréhension
l2 = [li**2 for li in l]
lpair = [li for li in l if li % 2 == 0]

# Slicing, indexation
l[start:step:stop]

# E.g. : inversion
l[::-1]

Faire attention aux constructions du type :

l = .... # une séquence
lsum = 0
for i in range(len(l)):
  lsum += l[i] 

Complexité en temps en \(O(|L|^2)\) pour une liste chaînée. Complexité en temps en \(O(|L|)\) pour un tableau.

Rappels de syntaxe de base

Les générateurs

Une construction de liste par compréhension est exécutée à la déclaration.

Pour économiser de la mémoire (e.g. permettant de représenter d’énormes structures), on peut utiliser des générateurs

l = [i**2 for i in range(10000000)] # Liste créée tout de suite

lsum = sum(l)

# Avec des générateurs
l = (i**2 for i in range(10000000)) # Liste créée tout de suite
sum(l)

Rappels de syntaxe de base

Sur les fonctions

Définition d’une fonction : def fname():


# Une fonction à 3 arguments
def f(a, b, c):
  ....
  return ...


f(1, 2, 3)

On peut utiliser un nombre variable d’arguments positionnels *args et/ou nommés **kwargs (kwargs = keyword arguments). Voir Python args and kwargs.


def f(*args, **kwargs):
  ....

# args = [1, 2, "a"]
# kwargs = {"bidule": "machine", "chose": 3}
f(1, 2, "a", bidule="machine", chose=3) 

Les lambda fonctions allègent la déclaration des fonctions :

fsq = lambda x: x**2

fsq(2)

Modificateurs de fonctions : les décorateurs

Voir https://www.pythoncheatsheet.org/cheatsheet/decorators

@your_decorator
def say_hi(name):
  print("Hi ! My Name is "+ name)

def your_decorator(func):
  def wrapper(*args,**kwargs):
    # Do stuff before func...
    result = func(*args,**kwargs)
    # Do stuff after func..
    return result
  return wrapper
@CountCallNumber
def say_hi(name"):
  print("Hi ! My Name is "+ name)

class CountCallNumber:
  def __init__(self, func):
    self.func = func
    self.call_number = 0

  def __call__(self, *args, **kwargs):
    self.call_number += 1
    print("This is execution number " + str(self.call_number))
    return self.func(*args, **kwargs)

Rappels de syntaxe de base

Loguer avec print: Pour déboguer un programme, on utilise bien souvent des print(...)

Que l’on peut combiner avec des f-strings :

def sqsum(l: list):
  return sum((li**2 for li in l))

l = [1, 2, 3]
print(f"La somme des carrés de {l} est : {sqsum(l)}")

Que l’on peut combiner avec des options de formattage :

import math

p = math.pi
# Produit un label de longueur 10, avec pi arrondi au millième
pilabel = f"La valeur de pi arrondi au millième est {math.pi:10.3f}"

print(pilabel)

Loguer avec logging : On peut aussi utiliser des loggers du module logging :

import logging

logging.basicConfig(format="%(levelname)s - %(asctime)s: %(message)s", 
                    datefmt= "%d/%m/%y %H:%M:%S", level=logging.INFO)

logging.info("Une info")
logging.warning("Un warning")
logging.error("Une erreur")

logging.basicConfig(filename="monlog.log",
                    format="%(levelname)s - %(asctime)s: %(message)s", 
                    datefmt= "%d/%m/%y %H:%M:%S", level=logging.INFO)

Lire/ecrire dans un fichier

De manière générale sur un texte en ASCII :

Lecture, avec un with statement:

with open("monfichier", "r") as fh:
  while True:
    line= fh.readline()
    if line == '':
      break

    process(line)

# Ou avec walrus
with open(fname, 'r') as fh:
    while((line:=fh.readline()) != ''):
      process(line)

Ecriture :

with open("monfichier", "w") as fh:
  montexte = f"La valeur de pi arrondi au millième est {math.pi:10.3f}"
  fh.write(montexte)

Lire/ecrire dans un fichier : Attention

Stocker des données numériques en ASCII

ATTENTION C’est une très mauvaise idée.

La valeur “1.23439949959” en ASCII occupe \(13 \times 8 = 104 bits\). Alors qu’un flottant peut occuper \(64\), \(32\), \(16\) bits.

Si ce sont des tableaux de valeurs, utilisez numpy.savez.

Sur des formats spécifiques yaml, xml, json, utilisez des parsers écrit pour le yaml, xml, json

Vous pouvez aussi construire votre propre format de fichier et utiliser fh.seek(), fh.read(num_bytes), struct.pack(), struct.unpack(). Voir struct

Ecrire un fichier en binaire

Sauvegarder \(500\) images de taille \(320 \times 240\) :

  • au format npz avec numpy nécessite 139 Mo, et 1.5 secondes pour accéder à une image,
  • avec un format binaire spécifique : 23 Mo, et 0.07 s pour accéder à une image.

with open("myfile", "rb") as fh:
  fh.read(8) 

Un format maison

Un format maison

Bibliographie

Références

Benureau, Fabien C. Y., and Nicolas P. Rougier. 2018. “Re-Run, Repeat, Reproduce, Reuse, Replicate: Transforming Code into Scientific Contributions.” Frontiers in Neuroinformatics Volume 11 - 2017. https://doi.org/10.3389/fninf.2017.00069.