.. testsetup:: from math import sin ========================== Eléments du langage Python ========================== 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. .. code:: sqrt(x) if x>=0 else sqrt(-x)*1J print("Bonjour" if lang=='fr' else 'Hello') Créer des fonctions =================== 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) 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 .. _decorateur 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 : .. testcode:: 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 : .. testcode:: @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 : .. testcode:: 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 : .. testcode:: 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) # doctest: +SKIP AssertionError: Argument should of type On peut mettre plusieurs décorateurs : .. testcode:: @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 : .. testcode:: 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 :func:`functools.lru_cache` du module :mod:`functools` effectue cette tache. .. exercice:: Modifiez le décorateur ``check_arg_type`` pour qu'il fonctionne avec un nombre arbitraire d'arguments. 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)) 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 "", line 1, in StopIteration .. _generateur: 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>> y = my_range(3) >>> type(y) >>> y.__next__() 0 >>> y.__next__() 1 >>> y.__next__() 2 >>> y.__next__() Traceback (most recent call last): File "", line 1, in 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). 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. .. math :: A = \sqrt{s(s-a)(s-b)(s-c)} où s est le demi-périmètre .. math :: s = \frac{1}{2}\left(a+b+c\right) 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 : .. testcode:: 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) # doctest: +SKIP 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. .. toctree:: :maxdepth: 2