Notes sur Python — deuxième partie

Ça faisait un petit moment qu’il n’y avait pas eu d’article, enfin, pour le blog comme pour beaucoup d’autres choses, on va dire que la nouvelle saison démarre plus près de début Février que début Janvier…

Du coup, la saison 2014 du Scylardor commence par la suite de mes quelques notes sur certaines particularités du Python !

OH MON DIEU un python !! Vite, il me faut étudier son fonctionnement !


Un peu de compréhension dans ce monde de brutes

Une des fonctionnalités phares du Python repose sur son principe de compréhension.

Il s’agit d’un sucre syntaxique permettant de créer facilement des listes, des dictionnaires… de manière raccourcie. À terme, elle permettent de remplacer des fonctions issues du paradigme fonctionnel telles que map() ou filter() (encore que ne les enterrons pas trop vite, elles ont leurs défenseurs).

For starters…

Comment fonctionne la compréhension ? Pour résumer, imaginons par exemple que nous voulions une liste des lettres allant de A à Z. Nous allons passer par trois étapes pour bien comprendre la transformation d’un état à l’autre.

Nous pourrions l’écrire comme ça:

Le Python, c’est for !

Ensuite, nous pourrions tirer parti du fonctionnement de l’instruction for de Python : contrairement aux langages comme C ou Java, en Python le for sert à itérer sur des… itérables (oui oui). Et en Python, beaucoup de choses sont itérables: les listes, les dictionnaires, et même les chaînes de caractères (au final, ce ne sont que des listes de caractères, non ?). La notation for nous permet notamment de nous affranchir de la variable i (qui sert d’index) et ressemblerait à ça :

Un exemple aidant à la compréhension des listes en compréhension comprenant une liste en compréhension

Déjà beaucoup plus concis et « pythonique« , comme on dit. Mais, enfin, nous pourrions aussi faire de l une liste en compréhension, c’est-à-dire utiliser les opérateurs de liste ( [ et ] ) et placer l’expression qui génère le contenu de la liste entre les deux. Cela ressemble en réalité plus à l’écriture mathématique de l’opération (voir par exemple l’article de Wikipédia) et permet de réduire la génération de l sur une seule ligne:

 

« C’est beau, c’est court, c’est Python ». Il est important de noter que l’appel à append est devenu inutile : on peuple la liste l au moment de sa déclaration. L’opération effectuée à l’intérieur des crochets revient exactement au même que ce qu’on a fait précédemment, et pourrait presque se traduire littéralement : « crée une liste et met dedans chaque lettre de l’alphabet ».

Si on veut dresser la liste des voyelles uniquement ? Aucun problème ! La syntaxe de la compréhension permet de spécifier des conditions avec if. Si on veut juste avoir la liste des voyelles dans l’alphabet (oui, là, c’est bête comme exemple, ralala) :

Après les listes, les dictionnaires

On peut même, sans problème, faire encore plus fort, comme des dictionnaires en compréhension ! Prenons par exemple une liste de tuples qui recense des pilotes et leur vaisseau:

Si on veut obtenir une relation clé-valeur où le nom du pilote est la clé, on peut simplement créer le dictionnaire en compréhension comme ceci :

Et ça aboutit à un joli dictionnaire. La clé des trucs en compréhension, c’est d’entourer l’ « expression de ce qu’on veut » ¹ par les caractères typiques du type de données qu’on souhaite obtenir (des crochets pour une liste, des accolades pour un dico…).

Sulu aime s’entourer d’ « expressions génératrices » (oh my)

Les paramètres étoilés: *args et **kwargs

En Python, il existe une notation spéciale pour couvrir la notion de fonctions prenant un nombre variable d’arguments : les paramètres étoilés * et ** (l’étoile est dans ce cas aussi appelée « opérateur splat« . Cette fonctionnalité permet une notation bien moins lourde qu’en C par exemple (où on doit utiliser le module va_arg pour cela), tout en ayant quelques autres avantages.

Les noms qu’on leur donne étant conventionnellement *args et **kwargs, je vais les reprendre dans cette partie.

Concrètement, comment ça fonctionne ? Pour résumer c’est très simple :

  • *args contient la liste des arguments variable passés à la fonction
  • **kwargs (pour KeyWords args) est quant à lui un dictionnaire (d’où le nom conventionnel). Il permet de définir le nom des paramètres variables (ou pas !) que l’on utilise à l’appel de la fonction. On y revient juste après

*(args)

Et ensuite, comment ça s’utilise ? Le cas d’école d’utilisation de telles fonctions, c’est par exemple printf : une fonction qui prend une chaîne de caractères de formatage (ou format string), et ensuite, supposément autant de variables que l’indique la chaîne de formatage. En abrégé pour ne garder que la partie qui nous intéresse, ça donnerait, en Python, quelque chose comme ça :

On voit là une utilisation typique de *args : une fonction qui prend au moins un paramètre connu, et ensuite, une suite, de taille variable, d’autres arguments.

Couplé au mécanisme de duck typing de Python, c’est une implémentation très facile du concept d’arguments variables. Notez bien que *args n’est pas la liste de tous les arguments de la fonction, mais uniquement ceux qui sont variables (fmt n’est pas dedans !).

**(kwargs)

**kwargs (Keywords Args) fonctionne sur le même principe, mais avec un dictionnaire.

Cette variable spéciale permet de déclarer des paramètres variables à la fonction qui ne vont pas être mis à la suite, mais stockés dans un dictionnaire où le nom procuré à l’appel sert de clé. J’en profite pour faire une parenthèse ( « (« , voilà, c’est fait) sur un autre mécanisme de Python un peu similaire : le nommage explicite des paramètres.

En effet, plutôt que de positionner les arguments lors d’un appel à une fonction, en Python on peut aussi explicitement spécifier à quel paramètre donne-t-on quelle valeur, sans se soucier de l’ordre, autrement dit passer des valeurs par nom plutôt que par position.

Et en fait, **kwargs ne stocke que les paramètres « variables », c’est-à-dire ceux qui n’ont pas un nom correspondant au nom d’un paramètre « fixe » de la fonction.

C’est un peu flou comme concept ? Ça va mieux avec quelques exemples :

Voilà donc en gros comment se servir des paramètres « étoilés » en Python. Ce sont deux notions importantes du langage².

Vous aussi, codez avec les étoiles en Python !

Vous aussi, codez avec les étoiles en Python !

 

Vous reprendrez bien une part de Python ?

Comment ça, ça donne pas envie ?

On va finir cet article sur une notion plutôt simple et bien pratique du Python, mais qui possède aussi son lot de subtilités : les indices en Python.

Des indices en-dessous de zéro (et pas que dans les pays où il fait froid)

Une particularité remarquable du Python est d’abord l’indexage négatif. C’est-à-dire qu’en Python, un index négatif fonctionne tout à fait !

« Mais… J’croyais qu’un index ça commençait à 0 ! Ça peut pas aller plus bas ! Et puis ça n’a pas de sens de compter avant le premier élément !! »

En effet, Python autorise l’utilisation des indices négatifs, et la logique derrière est très simple : il parcourt alors les éléments dans le sens inverse.

Lorsqu’on utilise des indices négatifs, plutôt que de « numéroter » en commençant « à gauche » (et là il faut s’imaginer une liste³ comme étant une structure allant de gauche à droite), et bien Python commence à numéroter à partir de la droite, c’est-à-dire à partir de la fin de la liste !

Par contre, comme -0 est égal à 0, il faut garder à l’esprit que l’indexage négatif n’est réellement possible qu’à partir de -1 : utiliser -0 équivaudra à utiliser 0, et donc, à pointer sur le premier (et non le dernier !) élément de la liste. Le dernier élément d’une liste correspond à l’index -1.

Une fois qu’on a compris ça, il est parfaitement possible de parcourir une liste à l’envers si on a envie:

On commence notre index à -1 (ça pointe sur le dernier élément), jusqu’à la taille de notre liste -1 (en négatif bien sûr) : on a bien parcouru la liste à l’envers.

Je vais te couper en tranches, espèce de liste !

Une autre astuce concernant l’indexage en Python concerne l’opérateur « :« , appelé « opérateur slice« , slice en anglais signifiant « tranche » ou « part ». Et pour cause : il permet littéralement de découper une liste en tranches !

Une utilisation courante est d’extraire une « tranche » d’une liste donnée, par exemple dans une liste de 8 éléments, récupérer les 4 au milieu:

Notez la subtilité: l’index à gauche du ‘:‘ est l’index du premier élément que vous voulez, et l’index à droite est celui du premier élément que vous ne voulez pas ! Ici par exemple, ‘Who’ est bien à l’index 2, mais l’index 6 contenant « Master », on ne l’a pas mis dans le retranchement. Les deux indices ne marchent donc pas pareil !

Personnellement, pour me rappeler de cette petite subtilité, je dis que l’opérateur slice (‘:‘) est « FILO » (un dérivé de LIFO, oui) :

  • First In : le premier index spécifié est l’index du premier élément qu’on veut « dans » le retranchement
  • Last Out : le dernier — second, dans notre cas… — index spécifié est l’index du premier élément qu’on ne veut pas dans le retranchement

Chacun son « truc » 🙂

Une autre utilisation, peut-être moins connue mais tout aussi puissante de l’opérateur slice, est de l’utiliser avec un seul index. Mais alors, qu’est-ce que ça fait dans ce cas-là ?

L’absence d’un index d’un côté ou de l’autre du « : » est en réalité un sucre syntaxique pour désigner « tout ce qui est avant/après cet index » : « tout ce qui va du début de ma liste jusqu’à cet index exclus » dans le cas de « :index » , et « tout ce qui va de cet index compris jusqu’à la fin » pour « index: » . Démonstration rapide:

Autres trucs pour bien hacher vos listes

Aussi, l’opérateur slice est beau, l’opérateur slice est fort, c’est donc tout naturellement que dans les cas où vous lui donnez des indices qui dépassent les limites de votre liste, il ne tentera jamais d’aller plus loin que le début ou la fin de la liste, selon le sens dans lequel vous voulez aller.

En plus, il permet également de modifier la liste en place et ce, pour inclure une sous-liste de n’importe quelle longueur à l’endroit que vous lui indiquez !

Pour finir, notons qu’il est tout à fait possible de combiner indices négatifs et opérateur slice, à condition que l’index à gauche du ‘:‘ soit toujours formellement (c’est-à-dire que dans le cas d’index négatif, il faut en fait penser à l’index positif correspondant) supérieur à celui de droite : l’opérateur slice ne permet pas de récupérer une tranche de liste à l’envers (pas comme ça, en tout cas…).

Quelques exemples pour montrer tout ça :

Niveau bonus : l’extended slicing

« Vous pensiez que c’était fini ? Et bien non ! »

Il serait dommage de parler du mécanisme de slice sans au moins prendre note qu’il existe l’extended slicing (on pourrait traduire ça par « découpage étendu »…).

Le principe, peu utilisé et méconnu et pourtant vieux (introduit avec Python 2.3 !) est simple : utiliser un deuxième ‘:’ suivi d’un troisième chiffre qui va servir de… « filtre multiplicateur » (je ne trouve pas d’autre mot ! :p). En effet, il permet, dans la « tranche » que nous avons découpé avec le premier ‘:’, de ne garder que les éléments dont l’index est un multiple de ce chiffre.

Ainsi, par exemple, liste[::1] renverra la liste inchangée. liste[::2] permet de ne prendre qu’un élément sur deux, etc. Et qu’arrive-t-il lorsqu’on utilise des chiffres négatifs ? Le même procédé s’effectue… Dans le sens inverse ! On peut donc inverser une liste avec par exemple liste[::-1].

Le mieux, c’est encore de le voir en action :

Nous sommes donc bien ici en présence d’un outil de manipulation de liste extrêmement puissant, qui permet d’effectuer un découpage selon un ou deux critères qui pourrait, écrit autrement, prendre plusieurs lignes ! Bon, après, OK c’est court, mais niveau concision du code, c’est pas top. Ce n’est pas très self-explanatory (difficile de dire au premier coup d’œil ce que fait ce code). C’est pas un hasard si peu de gens s’en servent… Mais c’est intéressant de savoir que ça existe !

 

Liens

¹ : je n’ai pas dit « expression génératrice » ici, car une expression génératrice ça crée autre chose : un générateur (on met alors l’expression entre parenthèses). Voir Wikipédia et l’article de Sam&Max sur les générateurs

² : pour un traitement plus exhaustif des arguments étoilés, Sam&Max (oui, encore eux) ont fait un très bon article dessus.

³ : notez qu’ici on parle de listes, mais tout ça est aussi valable pour les chaînes de caractères, qui ne sont jamais que des « listes de caractères » (outre le fait qu’il n’y ait pas de type « char » en Python, un caractère étant juste une string de longueur 1). Voir cette section des docs Python qui fait le tour de la question.

: voir la page des docs Python sur l’extended slicing.

——

Cet article touche à sa fin, on pourrait continuer longtemps comme ça, mais malgré que nous n’ayons vu (que dis-je, survolé !) que trois notions, il est déjà plutôt gros… 😮

Et il y a pourtant encore tant à dire ! Mais du coup, ce sera pour plus tard.

D’autres articles sur le Québec devraient bientôt arriver aussi, normalement.

En attendant, je vous laisse avec les Monty Python. Parce que.

 

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.