Généralités

  • Assignation
  • Représentation
  • Typage

par Rémy Mozul (remy.mozul@umontpellier.fr)

Commençons par quelques recommandations générales sur la manière de programmer avec le langage Python :

In [1]:
import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Pour reprendre quelques généralités déjà connus:

  • le signe = est l'opérateur d'assignation
  • à gauche du = est la variable
  • à droite du = est la valeur
  • les valeurs sont typé
  • toute valeur numérique a une représentation binaire
In [2]:
a = 12
b = -8
In [3]:
print( bin(a) )
print( bin(b) )
0b1100
-0b1000
In [4]:
bin(a)
Out[4]:
'0b1100'

Première remarque... nous essayons de reprénsenter des ensembles infinis ($\mathbb{N}, \mathbb{R}$, etc) dans une mémoire RAM finie...

Il se trouve que l'on perd quelques propritétés mathématiques devenue intuitives pour beaucoup:

In [5]:
a = -1.
b = 1.
c = 1e-24
d1 = a + ( b + c )
d2 = (a + b) + c
print( f"d1 == d2 ? \n -> {d1==d2}" )
d1 == d2 ? 
 -> False
In [6]:
print( f"d1={d1} et d2={d2}" )
d1=0.0 et d2=1e-24

Illustration de:

  • l'utilistion des boucles for en Python
  • de l'utilisation de la fonction "built-in" type (très utilse pour faire des vérifications)
  • de la liste des type numériques
In [7]:
for d in (True, 1, 1., 'c', 2.+1j):
    print( type(d) )
<class 'bool'>
<class 'int'>
<class 'float'>
<class 'str'>
<class 'complex'>

Pour rappel, la définition d'un bloc de code est décrite par l'apparition des : et de l'indentation à nombre d'espace égale sur tout les lignes qui appartiennent au bloc.

Donc pour faire une double boucle :

In [8]:
for i in range(3):
    for j in range(i, i+4):
        print( f"(i,j) = {i},{j}" )
    print( "passage au i suivant" )
(i,j) = 0,0
(i,j) = 0,1
(i,j) = 0,2
(i,j) = 0,3
passage au i suivant
(i,j) = 1,1
(i,j) = 1,2
(i,j) = 1,3
(i,j) = 1,4
passage au i suivant
(i,j) = 2,2
(i,j) = 2,3
(i,j) = 2,4
(i,j) = 2,5
passage au i suivant

Il faut donc être très vigilant sur l'utilisation des espaces pour l'indentation. Sinon on rencontre les erreurs suivantes :

for i in range(2):
    print( 'toto' )
  print(i)

File "<ipython-input-28-f9e84326b9e5>", line 3
    print(i)
            ^
IndentationError: unindent does not match any outer indentation level
for i in range(2):
for k in range(3):
    print( i, k )

File "<ipython-input-10-517c91c3ba15>", line 2
    for k in range(3):
      ^
IndentationError: expected an indented block

Ré-utilisation

Rappel:

$ u(x,t) = \sum_{n=1}^{+\infty} \left[ \frac{2}{l} \int_0^l \phi(x) sin( \frac{n\pi x}{l} ) dx \cdot e^{-\frac{n^2\pi^2}{l^2} D t} sin( \frac{n\pi x}{l} ) \right] $

Pour commencer, essayons simplement de définir une fonction qui permettent de calculer : $ f_n(x) = sin( \frac{n\pi x}{l} ) $

Il va falloir utiliser un module dédié, le module math, pour récupérer la valeur de pi ainsi que la fonction sinus. Pour avoir une liste exhaustive de ce qui est fourni par ce module, il faut aller voir ici : https://docs.python.org/3/library/math.html

La manière recommandée d'importer des sous parties de modules, est d'être le plus lisible possible. Ainsi il est fortement commandée de ne JAMAIS utiliser *. Mais plutôt de faire:

In [9]:
import math
p = math.sin( math.pi )

# ou a défaut:
from math import pi, sin
p = sin( pi )

Une fonction se définit avec le mot-clef def et ce qu'elle retourne avec return:

In [10]:
def f_n(n,x, l):
    result = math.sin( n * math.pi * x / l )
    print(f"calcul de f_n({n},{x},{l}) = {result} ")
    return result
In [11]:
N = 12
x0 = 1.2
L = 2*x0
f12 = f_n(N, x0, L)
calcul de f_n(12,1.2,2.4) = -7.347880794884119e-16 
In [12]:
# calcul pour plusieurs valeur de n
results = []
for x in range(4):
    new_value = f_n(x, x0, 3*L)
    results.append( new_value )
print( "Tout les résultats : ", results )
calcul de f_n(0,1.2,7.199999999999999) = 0.0 
calcul de f_n(1,1.2,7.199999999999999) = 0.5 
calcul de f_n(2,1.2,7.199999999999999) = 0.8660254037844387 
calcul de f_n(3,1.2,7.199999999999999) = 1.0 
Tout les résultats :  [0.0, 0.5, 0.8660254037844387, 1.0]

Dans l'exemple ci-dessus, une liste de résultats a été générée en plus afin de garder accès à toute les valeurs déjà calcuée précédemment.

De plus il faut faire attention au choix des variables à utiliser pour être lisible et expressif. Utiliser la variable de boucle x, alors qu'on le passe comme variable de mode est un très MAUVAISE idée. Il aurait mieux value faire:

In [13]:
for mode in range(4):
    new_v = f_n(mode, x0, 3*L)
    results.append( new_v )
calcul de f_n(0,1.2,7.199999999999999) = 0.0 
calcul de f_n(1,1.2,7.199999999999999) = 0.5 
calcul de f_n(2,1.2,7.199999999999999) = 0.8660254037844387 
calcul de f_n(3,1.2,7.199999999999999) = 1.0 

Représentation et discrétisation

Utilisation d'un module : matplotlib pour la représentation graphique

Première illustration (trop) simple : tracer une seule valeur en ordonnée:

In [14]:
# la ligne suivante est nécessaire uniquement
# dans l'environnement d'un notebook jupyter
%matplotlib inline

from matplotlib import pyplot as plt

plt.plot( f_n( 1, 0., L), 'b+' )
calcul de f_n(1,0.0,2.4) = 0.0 
Out[14]:
[<matplotlib.lines.Line2D at 0x7f044f91aa58>]

Pour afficher une courbe, il suffit de donner une liste de valeur. On remarque que l'abscisse est alors un index, decrivant la position du point dans la liste et pas une coordonnées en $x$ réelle.

In [15]:
results = list()
for x in range(8):
    results.append( f_n(1, x, L) )
    
plt.plot( results )
calcul de f_n(1,0,2.4) = 0.0 
calcul de f_n(1,1,2.4) = 0.9659258262890683 
calcul de f_n(1,2,2.4) = 0.49999999999999994 
calcul de f_n(1,3,2.4) = -0.7071067811865475 
calcul de f_n(1,4,2.4) = -0.8660254037844386 
calcul de f_n(1,5,2.4) = 0.2588190451025211 
calcul de f_n(1,6,2.4) = 1.0 
calcul de f_n(1,7,2.4) = 0.2588190451025208 
Out[15]:
[<matplotlib.lines.Line2D at 0x7f044f8abb38>]

La courbe précédente est continue uniquement parce que le graphique relie les valeurs calculée par des droites. Ce que nous avons calculé n'est bien qu'un nombre fini de points isolés les uns des autres :

In [16]:
plt.plot( results, '+' )
Out[16]:
[<matplotlib.lines.Line2D at 0x7f044f81eeb8>]

On peut modifier l'appel à plot pour donner en plus des ordonnés à tracer, la liste des abscisses associées. Mais d'abord modifions un peu la fonction f_n pour pouvoir supprimer le texte.

In [17]:
# redefinition de la fonction f_n pour modifier son comportement:
def f_n(n,x, l, log=True):
    result = math.sin( n * math.pi * x / l )
    if log:
        print(f"calcul de f_n({n},{x},{l}) = {result} ")
    return result

# calcul des nb_points abscisses entre 0 et 2pi
all_x = []
x_max = 2* math.pi
nb_points = 1001
for i in range(nb_points):
    all_x.append( i * x_max/(nb_points-1) )

# calcul des f_n associés à all_x
results = []
for x in all_x:
    results.append( f_n(1,x , x_max/2., False) )
    
# affichage
plt.plot(all_x, results)
Out[17]:
[<matplotlib.lines.Line2D at 0x7f044f79b5f8>]

Les tableaux numériques

Les listes sont des objets pratiques car polyvalents, mais pas du tout rapide d'accès. C'est là que les tableaux issus du module numpy interviennent.

In [18]:
def list_x(n):
    res = []
    for i in range(n):
        res.append(i)
    return res

import numpy as np
print( list_x(3)    )
print( np.arange(3) )
[0, 1, 2]
[0 1 2]
In [19]:
a = list_x(4)
b = np.arange(4)
print( type(a), type(b) )
<class 'list'> <class 'numpy.ndarray'>

Il est possible de mesurer le temps d'execution d'une ligne de code de python depuis les notebooks ou spyder avec l'instruction %timit. Cette fonctionnalité n'est pas disponible dans l'interpréteur standard.

Toutefois le résultat est assez édifiant:

In [20]:
N = 3000
%timeit( list_x(N)    )
%timeit( np.arange(N) )
10000 loops, best of 3: 159 µs per loop
The slowest run took 10.11 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 1.39 µs per loop

Il existe de nombreuses fonction disponible pour créer et manipuler des tableaux numpy : https://numpy.org/doc/stable/

Ici juste pour illustrer deux constructeurs proches:

In [21]:
print( f"L = {L}")
all_x1 = np.linspace(0,L,4)
print( all_x1 )
all_x2 = np.arange(0,L,0.8)
print( all_x2 )
L = 2.4
[ 0.   0.8  1.6  2.4]
[ 0.   0.8  1.6]

L'intérêt principaux des tableux numpy, est qu'ils permettent directement des manipulations arithmétiques ; ce que ne font pas les listes :

In [22]:
a = list_x(3)
b = list_x(4)
print( a+b )
[0, 1, 2, 0, 1, 2, 3]
In [23]:
a = np.random.rand(3)
b = np.random.rand(3)
print(f"a = {a}")
print(f"b = {b}")
c = 2*a + 3*b/2.
print(c)
a = [ 0.331102    0.86773669  0.8041845 ]
b = [ 0.43050854  0.4015414   0.5361088 ]
[ 1.30796681  2.33778547  2.4125322 ]

Ces opérations se font termes à termes sur les éléments des tableaux, il faut donc que les tailles des tableaux soient compatibles pour être possible.

Il est possible de vérifier quelle forme a un tableau avec l'attribut shape:

In [24]:
print( all_x1.shape )
print( all_x2.shape )
mat = np.array( [[1,2,3],[3,4,5]])
print( mat.shape )
print( mat )
mat[1,2] = 0
print( mat )
(4,)
(3,)
(2, 3)
[[1 2 3]
 [3 4 5]]
[[1 2 3]
 [3 4 0]]

On peut donc réecrire notre fonction f_n pour s'assurer qu'elle travaille automatiquement sur des vecteurs:

In [25]:
def f_n(n, x, l, log=True):
    r = np.sin( n * math.pi * x / l)
    if log:
        print(f"calcul de f_n({n},{x},{l}) = {r} ")
    return r

N = 100
all_x1 = np.linspace(0,L,N)
plt.plot( all_x1, f_n(1, all_x1, L, log=False) )
Out[25]:
[<matplotlib.lines.Line2D at 0x7f044f712048>]