3. Eléments du langage Python¶
3.1. Expression en Python¶
Dans un langage de programmation, on distingue les expressions des commandes. Les expressions vont être evaluer par l’interpréteur pour renvoyer un objet.
Nous n’allons pas faire une description exhaustive de toutes les possibilités.
Les parenthèses peuvent avoir plusieurs sens :
>>> sin(1 + 2) # appel d'une fonction 0.1411200080598672 >>> (1 + 2J)*3 # parenthèse logique (3+6j) >>> (1, 2J)*3 # n-uplet (1, 2j, 1, 2j, 1, 2j)
Nous vous rappelons la syntaxe dite de list comprehension :
>>> [i**2 for i in range(10)] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> [i//3 for i in range(10) if i%2==0] [0, 0, 1, 2, 2]
De même il existe des comprehension pour les dictionnaires et les ensembles :
>>> s = {chr(97+i) for i in range(10) if i%2==0} # ensemble >>> s == {'i', 'c', 'g', 'e', 'a'} True >>> d = {chr(97+i):i for i in range(10) if i%2==0} # dictionnaire >>> d == {'e': 4, 'c': 2, 'g': 6, 'i': 8, 'a': 0} True
Il possible d’utiliser une condition dans une expression sous la forme intuitive renvoie A si B sinon C. Ce type d’expression permet d’eviter de passer par des variables intermédiaires.
sqrt(x) if x>=0 else sqrt(-x)*1J print("Bonjour" if lang=='fr' else 'Hello')
3.2. Créer des fonctions¶
3.2.1. Arguments d’une fonction¶
Voici un exemple général de la définition d’une fonction :
>>> def f(a, b, c=1, d=2, *args, **kwd):
... print(a, b, c, d)
... print(args)
... print(kwd)
Cette fonction possède deux arguments obligatoires, deux arguments optionels. Les variable args et kwd vont contenir les arguments supplémentaires (sans et avec mots-clé - keyword).
Lorsque l’on appelle une fonction, les arguments peuvent être passés anonymement (par ordre) ou avec un nom (keyword argument, nom=valeur
). Il faut mettre d’abord les arguments anonymes puis les autres. Il n’y a pas de lien entre le fait qu’un argument ait une valeur par défaut et le fait qu’il soit utilisé avec son nom. Lorsque les arguments sont passés avec leur nom, l’ordre est indiférent :
>>> f(1, 2, 4)
1 2 4 2
()
{}
>>> f(b=2 , a=2)
2 2 1 2
()
{}
Les arguments en trop sont envoyés dans args ou kwd :
>>> f(1, 2, 3, 4, 5, 6)
1 2 3 4
(5, 6)
{}
>>> f(1, 2, 3, e=4)
1 2 3 2
()
{'e': 4}
>>> f(1, 2, 3, 4, 5, 6, e=7)
1 2 3 4
(5, 6)
{'e': 7}
Enfin, il est possible de séparer un groupe d’arguments à partir d’un itérable (list, tuple, ..) (séparation anonyme) ou à partir d’un dictionnaire (séparation avec mots-clés):
>>> liste = list(range(1,3))
>>> dct = {'d':3, 'e':4}
>>> f(0, *liste, **dct)
... # equivaut à f(0, 1, 2, d=3, e=4)
0 1 2 3
()
{'e': 4}
On remarquera que les variables args
et kwd
à l’intérieur de la fonction f
sont différentes de celles que l’on a séparées (liste et dct dans cet example).
Il est de toute façon possible de séparer plusieurs listes ou dictionnaires :
>>> print(*range(3), *range(3))
0 1 2 0 1 2
Quelques remarques :
Il ne faut par hésiter à utiliser des arguments par défaut (et c’est mieux que des variables globales)
Lorsque l’on appelle une fonction, il ne faut pas hésiter à nommer les arguments, même si c’est inutile et que c’est plus long à taper. Comparez
>>> scope.configure_channel(1, 0.01, 0.03, 50) >>> scope.configure_channel(channel_name=1, scale=0.01, offset=0.03, impedance=50)
3.2.2. Variable locale/globale¶
Les variables que l’on crée dans une fonction sont locale, c’est à dire indépendante d’une variable extérieur à la fonction qui porte le même nom.
>>> def f():
... x = 2
>>> x = 3
>>> f()
>>> x
3
A l’intérieur d’une fonction, une variable est soit locale soit globale
3.2.3. Décorateur¶
Il arrive fréquement que l’on veuille modifier ou rajouter des fonctionnalité à une fonction. Pour cela, il est possible en Python de créer une fonction qui renvoie une nouvelle fonction. C’est ce que l’on appelle in décorateur. Prenons l’exemple de cette fonctions :
def trace_fonction(f):
def wrapper(*args):
name = f.__name__
arg_str = ', '.join((str(elm) for elm in args))
print('Appel de la fonction {name}({arg_str})'.format(**locals()))
return f(*args)
return wrapper
from math import sin
sin = trace_fonction(sin)
Ce qui donne :
>>> sin(4)
Appel de la fonction sin(4)
-0.7568024953079282
Notez que dans la fonction wrapper
, le variable f
est globale tout en étant locale dans trace_fonction
.
Lorsque l’on définit la fonction soit même, on peut utiliser une syntaxe particulière : avant la ligne contenant mot clé def
, on indique le nom du décorateur précédé de @
. Par exemple :
@trace_fonction
def difference(x, y):
return x - y
Ce qui permet de s’amuser, et savoir comment python trie une liste:
>>> from functools import cmp_to_key
>>> liste = [2, 4, 6, 3, 7, 3]
>>> liste.sort(key=cmp_to_key(difference))
Appel de la fonction difference(4, 2)
Appel de la fonction difference(6, 4)
Appel de la fonction difference(3, 6)
Appel de la fonction difference(3, 4)
Appel de la fonction difference(3, 2)
Appel de la fonction difference(7, 4)
Appel de la fonction difference(7, 6)
Appel de la fonction difference(3, 4)
Appel de la fonction difference(3, 3)
Un exemple de décorateur est la fonction vectorize
du module numpy
, qui permet d’adapter n’importe quelle fonction à un tableau :
import math
import numpy as np
@np.vectorize
def my_sqrt(x):
if x>=0:
return math.sqrt(x) + 0J
else:
return math.sqrt(-x)*1J
Exercice
Créer une décorateur list_vectorize qui fait la même chose pour des listes
On peut aller un étape plus loin, ce que l’on met après l’arobase est n’importe quelle expression qui renvoie un décorateur. Par exemple, cela peut-être le résultat de l’appel d’une fonction. Par exemple :
def check_arg_type(arg_type):
def decorateur(f):
def wrapper(x):
assert isinstance(x, arg_type), 'Argument should of type {}'.format(str(arg_type))
return f(x)
return wrapper
return decorateur
@check_arg_type(int)
def fibonacci(n):
if n<=1: return n
return fibonacci(n-1) + fibonacci(n-2)
Ce qui donne :
>>> fibonacci(5)
5
>>> fibonacci(5.4)
AssertionError: Argument should of type <class 'int'>
On peut mettre plusieurs décorateurs :
@check_arg_type(int)
@trace_fonction
def fibonacci(n):
if n<=1: return n
return fibonacci(n-1) + fibonacci(n-2)
Ce qui donne :
>>> fibonacci(5)
Appel de la fonction fibonacci(5)
Appel de la fonction fibonacci(4)
Appel de la fonction fibonacci(3)
Appel de la fonction fibonacci(2)
Appel de la fonction fibonacci(1)
Appel de la fonction fibonacci(0)
Appel de la fonction fibonacci(1)
Appel de la fonction fibonacci(2)
Appel de la fonction fibonacci(1)
Appel de la fonction fibonacci(0)
Appel de la fonction fibonacci(3)
Appel de la fonction fibonacci(2)
Appel de la fonction fibonacci(1)
Appel de la fonction fibonacci(0)
Appel de la fonction fibonacci(1)
5
Evidement, on voit dans ce cas, que la façon de procéder n’est pas efficace, on peut alors décider de mettre en cache le résultat :
def cached(f):
memory = {}
def wrapper(x):
return memory.get(x, None) or memory.setdefault(x, f(x))
return wrapper
@cached
@check_arg_type(int)
@trace_fonction
def fibonacci(n):
if n<=1: return n
return fibonacci(n-1) + fibonacci(n-2)
Ce qui donne:
>>> fibonacci(5)
Appel de la fonction fibonacci(5)
Appel de la fonction fibonacci(4)
Appel de la fonction fibonacci(3)
Appel de la fonction fibonacci(2)
Appel de la fonction fibonacci(1)
Appel de la fonction fibonacci(0)
5
Note
La fonction functools.lru_cache
du module functools
effectue cette tache.
Exercice
Modifiez le décorateur check_arg_type
pour qu’il fonctionne avec un nombre arbitraire d’arguments.
3.3. Les boucles¶
Pour sortir d’une boucle on peut utiliser l’instruction break
, pour passer à l’itération suivante l’instruction continue
. Si un boucle se finit normalement, il est alors possible d’excuter un bloc d’instruction dans un else
. Voici un example:
def affiche_si_premier(n):
i=2
while i**2<=n:
if n%i==0:
print("{} n'est pas premier".format(n))
break
i = i+1
else:
print('{} est premier'.format(n))
3.3.1. Boucle for¶
Nous avons vu qu’il est possible de faire des boucles for sur des listes, des chaînes de caractères, des tuples. Il existe un concept général que l’on appelle itérateur. On va dire que les listes, chaînes de carctères et les tuples sont des itérateurs. Un itérateur est un objet qui possède une méthode __iter__
laquelle va renvoyer un objet (souvent le même) qui possède une méthode __next__
. On le verra plus en détail dans la partie orientée objet. Par exemple
>>> l = [0,1,2]
>>> x = l.__iter__()
>>> x.__next__()
0
>>> x.__next__()
1
>>> x.__next__()
2
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
3.3.2. Générateurs¶
Les générateurs sont utilisés pour faciliter la création d’itérateur. Il s’agit d’un fonction qui va renvoyer une série de résultat au lieu d’une simple valeur. On utilise la mot clé yield
. Un générateur ne pourra utiliser de return
:
>>> def my_range(n):
... i = 0
... while i<n:
... yield i
... i = i + 1
Une nouvelle valeur est renvoyée à chaque fois que yield est appelé :
>>> y = my_range(3)
>>> type(y)
<class 'generator'>
>>> y.__next__()
0
>>> y.__next__()
1
>>> y.__next__()
2
>>> y.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Dans le cas où l’on veut transformer une liste en une autre liste, cette méthode permet d’éviter de créer explicitement la liste. Par exemple
>>> def carre(l):
... for elm in l:
... yield elm**2
...
>>> for elm in carre(range(4)):
... print(elm)
0
1
4
9
Cette méthode permet aussi de parcourir les éléments d’une structure compliquée, comme un système 2D ou un arbre
def indices_tableau_2D(n, m):
for i in range(n):
for j in range(m):
yield i,j
Et une structure en arbre. Affiche l’ensemble des fichiers d’un repertoire ayant une extension donnée:
import os
def parcourt_repertoire(repertoire, ext):
for elm in os.listdir(repertoire):
elm_abs_path = os.path.join(repertoire, elm)
if os.path.isdir(elm_path):
for filename in parcourt_repertoire(elm_abs_path, ext):
yield filename
continue
if elm_abs_path.endswith(ext):
yield elm_abs_path
list(parcourt_repertoire('.', '.py'))
Exercice
Faire une fonction indices_tableau qui prend n argument et parcourt tous les indices de ce tableau nD. On utilisera des tuples pour les indices et on fera quelque chose de recursif (pour parcourir un tableau nD, on parcours un tableau (n-1)D le nombre de fois qu’il faut).
3.4. Les exceptions¶
Il arrive souvent lors de l’exécution d’un programme qu’une erreur apparaisse (par exemple prendre la racine carré d’un nombre négatif). Dans ce cas, Python arrête l’exécution du programme et affiche l’erreur. Ce comportement peut modifié : il est en effet possible de stopper la propagation de l’erreur. Ceci se fait à l’aide la structure try
except
from math import sqrt
a = -1
try:
b = sqrt(a)
except ValueError:
b = 0
Dans le cas où une fonction que l’on a créée ne peut pas être exécutée correctement, il est possible de créer et renvoyer une erreur.
def triangle_existe(a,b,c):
u""" Teste si le triangle de côté a,b,c existe """
pass
def aire_triangle(a,b,c):
u""" Calcule l'aire d'un triangle de côté a,b,c """
if not triangle_existe(a,b,c):
raise ValueError(u"Le triangle de côté {0},{1},{2} n'existe pas".format(a,b,c))
pass
Il existe plusieurs type d’erreur générique (ValueError
, NameError
, SyntaxError
). Dans la partie orientée objet, nous verrons comment créer son propre type d’erreur.
Un cas fréquent consiste à modifier attraper une erreur et ensuite à renvoyer une nouvelle erreur. Par exemple, la formule de Héron permet de calculer l’aire d’un triangle en connaissant uniquement la longueur des trois côtés a,b et c.
où s est le demi-périmètre
Dans le cas où le triangle n’existe par, l’argument de la racine carré est négatif. Le module math
va renvoyer une ValueError
. Il est possible de modifier cette erreur et de renvoyer un message plus explicite :
from math import sqrt
def aire_triangle(a, b, c):
s = (a+b+c)/2
try:
return sqrt(s*(s-a)*(s-b)*(s-c))
except ValueError:
msg = "Le triangle de côtés {a}, {b} et {c} n'existe pas"
raise ValueError(msg.format(a=a, b=b, c=c))
Ce qui donne :
>>> aire_triangle(3, 4, 5)
6.0
>>> aire_triangle(1, 1, 10)
ValueError: Le triangle de côtés 1, 1 et 10 n'existe pas
Note
Il est plus efficace d’utiliser un try ... except
que de faire un test préalable (qui peut parfois être compliqué). C’est un comportement non-intuitif. En Python, il est plus efficace de traverser la route les yeux fermé et de voir ce qu’on fait si on a un accident que de regarder si il y a une voiture qui passe avant de traverser. Cette façon de procéder peut être indispensable dans certaine situation. Par exemple :
>>> try:
... open('un_fichier_qui_n_existe_pas')
... except FileNotFoundError:
... print('Erreur : entrez un nouveau fichier')
Erreur : entrez un nouveau fichier
En effet, si on teste l’existence du fichier, il est toujours possible (bien que peu probable) que le système d’exploitation suprime le fichier entre le test et l’ouverture du fichier.