Python

Message 1, par Elzen

§ Posté le 27/10/2014 à 18h 12m 30

Ce week-end (pour les talks, et aujourd'hui et demain pour les sprints(1)) se tenait la PyConFr, c'est-à-dire la conférence francophone des utilisateurs du langage Python. Comme c'était près de chez moi et que je fais quand même pas mal de Python, je suis évidemment allé y faire un tour ; et j'ai décidé à cette occasion de consacrer un article à ce langage que j'ai déjà brièvement évoqué ici à plusieurs reprises, et dont le nom, soit dit en passant, est un hommage aux Monty Python, un célèbre groupe d'humoristes anglais(2).

Python est donc un langage de programmation, pas tout jeune (la première version date de 1990), mais est resté relativement discret au début, tandis qu'il devient actuellement de plus en plus populaire. Si vous savez déjà programmer dans un autre langage, cet article pourra vous aider à retrouver vos marques. Dans le cas contraire, je vous conseille de jeter un œil à cet article, où j'expliquais la notion de langage de programmation : il s'y trouve un lien vers un livre en PDF, destiné aux gens voulant apprendre la programmation, et utilisant précisément Python : ce sera sans doute plus clair pour vous que ce qui va suivre.


Python est interprété, ce qui très pratique pour les scripts ; mais le langage est également un bon candidat pour des programmes plus sérieux(3). L'interpréteur de base du système permet d'ailleurs de l'utiliser de façon interactive, un peu comme un shell, donc n'hésitez pas à le lancer pendant votre lecture de cet article, pour expérimenter.

Notez d'ailleurs que vous pouvez, dans cet interpréteur, utiliser la fonction help pour obtenir la page de documentation du type qui vous intéresse (par exemple, « help(str) »). N'oubliez pas d'y taper import antigravity 😊


Par rapport aux langages les plus courants

Parmi tous les langages de programmation existants, Python présente une caractéristique à la fois très intéressante et très peu commune(4) : celle de donner une fonction sémantique à l'indentation (là encore, si ce concept ne vous est pas familier, jetez un œil à cet article).

Là où, dans les autres langages, on utilise des accolades ou des mots-clefs spécialisés pour fermer les différents blocs (« fi », « endif »…), et où l'indentation ne sert donc qu'à faciliter la lecture aux personnes qui regardent le code, python se distingue donc en utilisant cette indentation pour délimiter les différents blocs : en cas d'erreur, la machine et le lecteur se trompent donc de la même façon, ce qui aide à comprendre le soucis.

Un exemple rapide (qui n'a pas vraiment de sens, c'est uniquement pour montrer la différence de syntaxe) entre ce qui pourrait fonctionner, par exemple, en C ou en Java(5), et la façon dont on l'écrit en Python :

if (a == b) {
    a += 2;
    b += 4;
}
c = a + b;
if a == b:
    a += 2
    b += 4
c = a + b

Autre remarque, le code à accolade présenté ci-dessus ne pourrait pas fonctionner tel quel en C ou en Java : d'une part, il faudrait encapsuler ça proprement (dans une fonction main en C, dans la méthode main d'une classe en Java) ; d'autre part, il faudrait commencer par déclarer les variables a, b et c en précisant de quel type elles sont.

En Python, pas besoin : d'une part, on peut écrire du code directement en vrac dans un fichier simple(6) ; d'autre part, il n'y a pas de typage précis obligeant à déclarer ses variables plus ou moins à l'avance : la variable est créée la première fois qu'on l'affecte, et c'est tout (bon, en l'occurrence, il faudrait quand même initialiser a et b avant).


Python propose également un mécanisme d'initialisation à partir d'éléments multiples assez intéressant : si, comme on en a l'habitude par ailleurs, on peut affecter un tuple de cette manière :

t = a,b,c,d

On peut également procéder à l'opération inverse assez simplement :

a,b,c,d = t

Et affecter, donc, les quatre variables à partir du contenu du tuple. Ce qui permet d'échanger le contenu de deux variables en une seule opération, plutôt que de devoir passer par une variable intermédiaire comme ailleurs :

a,b = b,a

Voilà pour les quelques éléments qui pourront surprendre les habitués à d'autres langages. Passons maintenant à ce que l'on croisera le plus souvent dans ce langage :


Types de base et conversions

Disposant d'une bibliothèque standard, Python propose évidemment un certain nombre de types et de fonctions utiles que l'on peut aller chercher en cas de besoin. Mais certains d'entre eux sont beaucoup plus fréquents que d'autres.

En premier lieu, évidemment, les nombres, pour lesquels on croise surtout les types int et float, et les chaînes de caractères. En Python 2, il y avait deux sortes de chaînes : d'un côté, str, qui représentait un flux brut venu de l'extérieur, et de l'autre unicode, qui représentait une suite de chaînes de caractères interne au programme, correctement décodée. Cela a été simplifié dans Python 3, où le type str est désormais le principal.


Attention : quoique le typage soit dynamique, il n'y a pas de conversions implicites. Si vous écrivez « 1 + "1" », ça n'essayera pas de convertir la chaîne en entier ou l'entier en chaîne, ça générera une exception.

Il faut donc utiliser les fonctions de conversions de base. C'est assez simple : str(variable) renverra une chaîne de caractère automatiquement constituée à partir de la variable passée, quelle que soit son type. De même, int(variable) (pour lequel on peut éventuellement préciser une base en second paramètre, par exemple) et float(variable) tenteront de convertir la variable en entier ou en nombre à virgule.

Notez que cela ne marchera pas uniquement pour les types de base : on peut définir, dans les objets, des fonctions adéquates pour effectuer des conversions de la même manière, j'y reviendrai en vous parlant d'objets.

Il y a également, bien sûr, des booléens et une valeur nulle, qui s'écrivent ici avec une majuscule : True, False et None.


Les autres types courants sont les collections : un tuple représente un tableau immutable (son contenu ne pourra pas être modifié après sa première affectation), et une list représente une liste d'éléments, que l'on va pouvoir modifier. Par ailleurs, le dict représente un dictionnaire, c'est-à-dire un tableau associatif, pour lequel on peut associer des valeurs à des clefs de toutes sortes de types. Enfin, les set sont des ensembles, c'est-à-dire des collections d'éléments sans doublons, mais dont l'ordre n'est pas déterminé (on ne peut pas demander à accéder au énième élément).

Ces quatre types sont tellement courants qu'ils peuvent être initialisés très simplement :

t = ("ceci", "est", "un", "tuple")
l = ["ceci", "est", "une", "liste"]
d = {"ceci": "est", "un": "dictionnaire"}
s = {"ceci", "est", "un", "ensemble"}

Attention, puisque ensembles et dictionnaires se déclarent de deux façons assez proches : si vous demandez juste {}, cela crée un dictionnaire vide. Pour obtenir un ensemble vide, il faut passer par une fonction plus explicite : set().

Tant qu'on est dans les pièges : (valeur) ne crée pas un tuple contenant la valeur. Il faut pour cela écrire ceci : (valeur,) car les parenthèses seules sont également utilisées comme délimiteur (elles permettent, par exemple, de placer une seule expression sur plusieurs lignes, pour les cas où l'indentation sémantique serait problématique).


Un autre objet relativement courant, quoique rarement explicitement, est le generator : il s'agit d'une sorte de liste dont les éléments ne sont pas connus à l'avance, mais calculés au fur et à mesure, ce qui est utilisé pour les parcours, par exemple. D'ailleurs, à ce sujet :


Structures de contrôle

Python se veut lisible : les structures de contrôle s'efforcent donc d'utiliser moins de caractères sibyllins que d'autres langages. En particulier, les opérations booléennes sont appelées par leur nom : or pour le ou logique et and pour le et logique. On écrit donc « if condition1 and condition2 ».

L'opérateur de test mono-ligne, permettant de donner plusieurs valeurs en fonction d'une condition, s'utilise également très simplement :

nb = 1 if condition else 2

Les deux boucles habituelles for et while sont bien sûr disponibles : le second sert à répéter une suite d'opérations tant qu'une condition est validée ; tandis que le premier sert à effectuer une suite d'opérations pour chaque élément d'une liste ou d'un générateur(7). On écrit donc :

while condition:
    instructions
for variable in liste:
    instructions

Ce second opérateur peut d'ailleurs être lui aussi utilisé en mono-ligne, pour constituer un générateur à partir d'une liste (ou d'un autre générateur) :

doubles = (i*2 for i in simples)

Notez que les parenthèses sont ici requises.


Le mot-clef in peut cependant être utilisé également pour les conditions classiques : en dehors de la syntaxe particulière d'une boucle for, « variable in liste » est évalué à vrai quand la variable est l'un des éléments de la liste, et à faux sinon.

Par ailleurs, quelques fonctions de base pouvant vous être utile : range, appelée avec un seul paramètre entier, renvoie une liste d'entiers allant de zéro jusqu'à cet entier, zéro inclus, et la valeur maximale exclue(8) ; et len vous renvoie la longueur d'une collection (ou d'une chaîne de caractère). Pour un parcours classique des indices d'une liste, on utilisera donc classiquement :

for i in range(len(liste)):
    instructions


Par ailleurs, la gestion des exceptions se fait classiquement avec le mot-clef try(9) :

try:
    instructions à risque
except Exception as e:
    traitement


Définition de fonctions

Python propose deux manières de définir une fonction. En premier lieu, vous avez la version classique, en plusieurs lignes, qui va s'écrire ainsi :

def fonction(param1, param2="default", *args, **kwargs):
    instructions

Détaillons un peu : dans cet exemple particulier, la fonction prendra nécessairement un paramètre param1 (une exception sera déclenchée s'il n'est pas présent). Un paramètre param2 peut également être indiqué, mais il sera optionnel : s'il n'est pas précisée, la valeur par défaut fournie sera utilisée. Les deux trucs bizarre, juste derrière, indique que vous pouvez fournir autant de paramètre supplémentaires que vous voulez.

Si vous appelez votre fonction avec, par exemple, « fonction(1, 2, 3, 4, 5) », les deux premiers paramètres auront, respectivement, les valeurs 1 et 2, et le tuple args contiendra les valeurs supplémentaires (3, 4 et 5). Vous pouvez également préciser un nom spécifique à chaque paramètre : « fonction(1, 2, a=3, b=4, c=5) » fera en sorte que le dictionnaire kwargs contiendra les clefs a, b et c, avec les valeurs indiquées.

Bien sûr, vous pouvez tout à fait définir une fonction n'utilisant qu'un nombre fixe de paramètres, sans cas bizarres de ce genre, en retirant simplement la fin.


À l'intérieur de la fonction, vous pouvez définir une valeur de retour à l'aide du classique mot-clef return. Si celui-ci n'est pas mentionné, la fonction sera considérée comme renvoyant None… à moins que vous n'utilisiez un autre mot-clef, yield. Dans ce cas, la fonction renverra un générateur, et la valeur suivant ce mot-clef sera celle renvoyée au énième appel.


L'autre manière de définir une fonction est quelque chose de plus condensé, utilisée par exemple pour préciser une fonction de comparaison pour les tris : elle permet de définir et de renvoyer une fonction en une seule ligne. Cela fonctionne ainsi :

fonction = lambda argument: valeur

Et c'est rigoureusement identique à ceci :

def fonction(argument):
    return valeur

À la différence près, bien sûr, que vous pouvez utiliser la fonction directement plutôt que de la stocker dans une variable.


Création d'objets

Python vous permet de définir des objets assez simplement, et supporte quelques aspects intéressants, comme l'héritage multiple. Basiquement, déclarer une nouvelle classe se fera ainsi :

class MaClasse(parent1, parent2):
    def __init__(self, *args):
        instructions

Les parents indiqués entre parenthèses sur la première ligne sont les classes-mères. Il est conseillé, si vous n'héritez de rien en particulier, de tout de même préciser la classe de base object à cet endroit.

La deuxième ligne définit la première méthode de notre classe, qui sera son constructeur, appelé automatiquement lorsque l'on crée une nouvelle instance. Placez-y donc les instructions d'initialisation de l'objet.

On remarque, dans les paramètres de cette classe, un self. Il sert à désigner l'objet courant. En effet, en Python, les deux appels suivants sont rigoureusement équivalents (le premier étant en fait un alias du second) :

monObjet.methode()
MaClasse.methode(monObjet)

En d'autres termes, la référence vers l'objet courant sera le premier paramètre passé à votre méthode, avant les éventuels autres paramètres. Vous pouvez donc, d'ailleurs, lui donner le nom que vous désirez, même si self est le nom conventionnel, généralement mis en valeur par la coloration syntaxique.


Vous pouvez ensuite définir autant d'autres méthodes que vous le désirez, certaines étant utilisées par le langage pour accomplir quelques fonctions usuelles. Par exemple, si vous tentez de convertir votre objet en chaîne de caractère (« str(monObjet) », appelé automatiquement lors d'un print par exemple), le système cherchera et appellera la méthode __str__ si celle-ci est présente. Vous pouvez ainsi redéfinir pas mal d'opérations de base à votre convenance.


Une fois la classe définie, vous voudrez sans doute l'utiliser. Il suffit dans ce cas d'utiliser le nom de la classe comme une méthode d'initialisation. Dans l'exemple ci-dessus, par exemple, l'objet sera créé ainsi :

monObjet = MaClasse()


Oh, pour les méthodes des objets comme pour les fonctions indépendantes, n'oubliez pas de décrire leur fonctionnement en commentaire (avec un caractère « # » au début), juste avant ou juste après la ligne contenant le « def » : ces commentaires seront utilisés automatiquement par Python pour générer la documentation de votre code (ça marchera pour la fonction help dont je vous parlais au début, notamment).

Alternativement, cela peut également être obtenu avec une chaîne de caractère multilignes (obtenue délimitée par trois guillemets doubles d'affilée) en lieu et place des commentaires.

Erratum (merci à Gordon pour la relecture) : le commentaire avant la déclaration de la fonction est reconnu par help, mais pas par d'autres fonctions. Après la déclaration, il ne l'est dans aucun cas. Utilisez plutôt ces chaînes de caractères multilignes, qui sont la bonne manière de documenter.


Remarques personnelles : quelques défauts

J'aime beaucoup le Python. C'est très agréable à coder, et les quelques éléments sus-mentionnés permettent de faire plein de choses de manière plus simple que dans d'autres langages. Néanmoins, il y a quelques petits détails qui me chiffonnent, et dont je préfère quand même vous faire part.


D'une part, j'ai tendance à considérer que l'interprété est très bien pour faire des tests et des scripts, mais, quand il s'agit de quelque chose de sérieux, j'ai tendance à préférer compiler(10). De même, la démarche de séparer compilation et exécution, la première pour vérifier que la syntaxe et les usages généraux sont corrects, et la seconde pour tester réellement le code, me semble importante pour l'apprentissage.

Ceci dit, sur ce point-là, on peut s'arranger : il existe un outil, Cython, qui est initialement conçu pour créer facilement des modules python compilés en C, mais qui peut également être utilisé pour compiler le code principal.


D'autre part, il manque la notion de visibilité sur les objets : quand on veut écrire un attribut ou une méthode qui ne soit qu'interne, on lui donne par convention un nom commençant par le caractère « _ », histoire de prévenir les autres gens qu'il vaut mieux éviter de l'utiliser directement ; mais il n'existe aucun mécanisme du langage permettant réellement d'empêcher que le truc soit utilisé à l'extérieur.

Cette fois encore, cela me semble important pour un usage réel, mais également dans un cadre pédagogique : d'après mon expérience, apprendre à distinguer clairement ce qui est uniquement interne à l'objet et ce qui est ouvert sur l'extérieur est une étape importante pour bien comprendre la notion d'objet.


De tierce part, le typage dynamique est très pratique pour, par exemple, des collections contenant plusieurs sortes d'éléments ; mais la rigueur du typage statique est préférable dans certains autres cas. Notamment, il serait préférable de pouvoir spécifier, dès la définition d'une fonction, quels types d'arguments elle attend. Ou bien qu'une éventuelle exception se déclenche au moment où on affecte une valeur incorrecte à une variable, et non pas au moment, quelques lignes plus bas, où on tente de l'utiliser.

Dans un cadre pédagogique, une fois encore, la démarche de commencer par réfléchir aux variables dont on va avoir besoin, et des types qu'elles recevront ; et l'impossibilité d'utiliser une même variable pour mettre successivement deux trucs radicalement différents, est assez importante(11).


Si quelqu'un connaissait donc un langage extrêmement proche de Python, mais qui serait compilé, avec du typage statique (quoique pas forcément obligatoire) et des visibilités sur les objets, je vous saurais gré de bien vouloir me le présenter : ce serait vraisemblablement, à mes yeux, le langage pédagogique idéal (enfin, je me fais confiance pour trouver quelques raisons de râler contre quand même ^^").


Message 2, par grim7reaper

§ Posté le 06/11/2014 à 22h 12m 57

Citation (Elzen)

Comme cela a été rappelé, rapports d'expérience à l'appui, lors de la PyConFr, il arrive dans certains cas que les performances d'un code python, interprété, soient proches de, voire meilleures que, celles d'un code C compilé.

Ça n’a pas grande valeur, dans certains cas bien choisi un langage A pourra être plus rapide qu’un langage B.

Il y a des exemples à la pelle sur le Web où Java, Python, Haskell, le langage "X" sont plus rapide que le C. Mais dans l’ensemble, le C est plus rapide.


Citation (Elzen)

À ma connaissance, le seul autre langage à utiliser les caractères blancs plutôt que de les ignorer.

Haskell aussi donne une fonction sémantique à l’indentation.


Citation (Elzen)

Il y a également, bien sûr, des booléens et une valeur nulle, qui s'écrivent ici avec une majuscule : True, False et None.

À ce sujet, petite anecdote : en Python 3 True et False sont des mots-clefs (keyword.iskeyword('True') renvoie vrai) du langage, pas en Python 2.

Du coup, en Python 2 tu peux faire

True, False = False, True

Ce qui risque de casser beaucoup de code 😋


Citation (Elzen)

Pour obtenir un ensemble vide, il faut passer par une fonction plus explicite : set().

Il existe aussi tuple(), list() et dict()


Citation (Elzen)

dans tous les autres langages, try est un mot-clef réservé, et on ne peut donc pas déclarer une fonction portant ce nom.

Nope, c’est pas le seul langage où try n’est pas un mot-clef.

Le premier qui me vient à l’esprit est Ada (qui a bien des exceptions), puis Ruby.

Sinon Go et Rust n’ont pas de mot-clef try, mais bon ils n’ont pas d’exceptions (mais Rust à une macro try!).

Après quelques recherches, j’ai vu que Objective-C utilisait @try donc il doit être possible de déclarer une fonction try je suppose


Citation (Elzen)

Python propose deux manières de définir une fonction.

J'aurai précisé que les lambda en Python sont plus limité que les fonctions (elles ne peuvent contenir que des expressions, pas d’instructions).


Citation (Elzen)

Il est conseillé, si vous n'héritez de rien en particulier, de tout de même préciser la classe de base object à cet endroit.

Oui, il vaut mieux en effet, sinon certaine choses risque de ne pas fonctionner.


Citation (Elzen)

La deuxième ligne définit la première méthode de notre classe, qui sera son constructeur

Non, la méthode __init__ est un initialiseur (comme son nom l’indique), pas un constructeur (ça serait plutôt la méthode __new__).


Citation (Elzen)

Utilisez plutôt ces chaînes de caractères multilignes, qui sont la bonne manière de documenter.

Aussi appelé docstrings.


De manière globale, j’aurais mentionné que tu parles principalement de Python 2, car certaines petites choses diffèrent en Python 3 (range renvoie un générateur par exemple, l’héritage de object est automatique)).


Sinon, au sujet du typage statique en Python, Guido van Rossum lui-même y est favorable (peut-être pour Python 3.5), probablement en s’inspirant de mypy. Mais dans tout les cas, ça resterait optionnel.


Enfin, je ne sais pas si le fait qu’un langage soit objet soit un critère important pour en faire un bon langage pédagogique (la POO c’est un paradigme parmi d’autres, et c’est pas le graal non plus (des qualités mais aussi beaucoup de défauts)).

Message 3, par Elzen

§ Posté le 07/11/2014 à 14h 27m 31

Merci pour ces rectifs/précisions (dont certaines auraient été dans l'article si j'avais trouvé comment le dire bien sur le coup)


Tu connais plus de langages que moi 😊



Juste, concernant le dernier point :

Citation (grim7reaper)

Enfin, je ne sais pas si le fait qu’un langage soit objet soit un critère important pour en faire un bon langage pédagogique (la POO c’est un paradigme parmi d’autres, et c’est pas le graal non plus (des qualités mais aussi beaucoup de défauts)).

Je suis on ne peut plus d'accord sur le fait que la POO n'est pas l'alpha et l'oméga et présente pas mal d'aspects pas géniaux ; d'ailleurs, ce que l'on fait n'est pas de la POO, mais de l'impératif avec des objets dedans.


Mais il faut faire la part des choses en fonction du public visé : soit il s'agit d'un cursus réellement informatique, auquel cas il me semble qu'il faut une présentation de chacun des quatre grandes familles de paradigmes (impératif, orienté objet, fonctionnel et par contraintes ; je regrette toujours autant que ma propre scolarité ait été si succincte sur le troisième et ait fait l'impasse sur le dernier, d'autant que je me rapproche de plus en plus de celui-ci…).

Soit il s'agit d'une initiation à la programmation adressée à des étudiants en autre chose (ou en plein de chose à la fois avant spécialisation, comme là où j'enseigne), auquel cas fonctionner comme ça (impératif avec objets) est relativement simple et a l'avantage de leur permettre de comprendre la plus grosse partie de ce à quoi ils seront le plus souvent confrontés par la suite.


Bref, le critère que je pointais n'est pas qu'un langage soit objet ; il est que, étant donné que les objets sont (à tort ou à raison) au programme, il faut que le langage permette de voir ce qu'il y a à voir dessus, dont la visibilité.

Message 4, par grim7reaper

§ Posté le 07/11/2014 à 21h 23m 13

Citation (Elzen)

Soit il s'agit d'une initiation à la programmation adressée à des étudiants en autre chose (ou en plein de chose à la fois avant spécialisation, comme là où j'enseigne)

Ha ok, je pensais que tu donnais des cours à des étudiants en cursus informatique.

(Suite au décès inopiné de mon précédent serveur, je profite de mettre en place une nouvelle machine pour essayer de refaire un outil de blog digne de ce nom. J'en profiterai d'ailleurs aussi pour repasser un peu sur certains articles, qui commencent à être particulièrement datés. En attendant, le système de commentaires de ce blog n'est plus fonctionnel, et a donc été désactivé. Désolé ! Vous pouvez néanmoins me contacter si besoin par mail (« mon login at ma machine, comme les gens normaux »), ou d'ailleurs par n'importe quel autre moyen. En espérant remettre les choses en place assez vite, tout plein de datalove sur vous !)