Aller au contenu

Le langage Python

Le langage Python est un langage de programmation interprété, interactif, amusant, orienté objet (souvent), fonctionnel (parfois), open source, multi-plateformes, flexible, facile à apprendre et difficile à maîtriser. Il a été développé et publié en 1991 pour Guido van Rossum, qui cherchait à créer un langage amateur pour s’occuper le week-end et durant les fêtes de Noël footnote:[https://www.python.org/doc/essays/foreword]. Le projet a décollé et Python est maintenant considéré comme l’un des langages de programmation les plus populaires. Il est géré en partie par la Python Software Foundation, créée en 2001, et en partie par différents sponsors, dont HPE, Intel et Google. Django, que nous verrons plus loin, est soutenu par la Django Foundation. Celle-ci est alimentée par des volontaires et recevait (en 2013) moins de $50 000 en donations directes.

Suivant les langages que vous connaitriez ou auriez déjà abordé, certains concepts pourraient être difficiles à aborder :

  • L’indentation définit l’étendue d’un bloc (classe, fonction, méthode, boucle, condition). Il n’y a pas d’encapsulation de la portée ou visibilité de variables ou fonctions par une double accolade: seule l’indentation définit l’étendue d’un morceau de code.
  • Il n’y a pas de typage fort des variables ; une chaîne de caractères x peut ainsi devenir un entier quelques lignes plus loin.
  • Il n’y a pas de compilation avant exécution ; il n’y a donc pas de validation statique du code, pour assurer le filet de sécurité avant la mise en production,
  • Le langage évolue beaucoup (et il s’agit d’un langage qui est plus vieux que Java). Il est parfois difficile de changer ses habitudes pour adopter une manière plus récente de faire parfois la même chose.

Malgré ces quelques points, Python reste un langage généraliste accessible et “bon partout”, et peut se reposer sur un écosystème stable et fonctionnel, qui tourne grâce avec un système d’améliorations basées sur des propositions: les PEP, ou “Python Enhancement Proposal”. Chacune d’entre elles doit être approuvée par le Benevolent Dictator For Life.

Il peut aussi se targuer d’une courbe d’apprentissage relativement linéaire, sans avoir à connaître ou appliquer des concepts. Un exemple simple de Hello World en langage C, qui nécessite de comprendre les principes d’include, de fonction main(), de valeur de retour (int ou void), de fin de statement (;) et de scope ({...}) :

#include <stdio.h>
int main() {
printf("Hello, World!");
return 0;
}

peut ainsi se traduire en une seule ligne :

print("Hello, World")

En fonction de votre niveau d’apprentissage du langage, plusieurs ressources pourraient vous aider ou intéresser :

  • Pour les débutants, Automate the Boring Stuff aka. Practical Programming for Total Beginners, qui aborde énormément d’aspects du langage en détails,
  • Pour un (gros) niveau au dessus et pour un état de l’art du langage, nous ne pouvons que vous recommander le livre Expert Python Programming qui aborde les différents types d’interpréteurs, les éléments de langage avancés, différents outils de productivité, métaprogrammation, optimisation de code, programmation orientée évènements, multithreading et concurrence, tests, … A ce jour, c’est le concentré de sujets liés au langage le plus intéressant qui ait pu arriver entre nos mains.

En parallèle, si vous avez besoin d’un aide-mémoire ou d’une liste exhaustive des types et structures de données du langage, référez-vous à la Python Cheat Sheet.

Guide de style (PEP 8)

Cette spéfification réalise une proposition sur la manière d’organiser et formater du code Python, quelles sont les conventions pour l’indentation, le nommage des variables et des classes, … Il s’agit d’une proposition concrète concernant la lisibilité du code - sachant que du code est plus lu qu’écrit (ce qui nous ramène à la notion d’environnement.

En bref, elle décrit comment écrire du code proprement, afin que d’autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité. Comme l’indique la PEP20, Readibility counts. Ceci implique de garder une cohérence dans l’écriture du code, dont les principales recommandations concernent:

  • Le layout général du code: indentation, longueur de ligne, séparateurs de lignes, encodage des fichiers sources et gestion des imports.
  • La gestion des commentaires, en fonction de leur emplacement: blocs de commentaires, commentaires embarqués ou documentation.
  • Les conventions de nommage : les noms à éviter, comment nommer une classe, une fonction ou une variable, un paquet, les exceptions ou les constantes.
  • Des recommandations de programmation, concernant le typage statique

Pour vous aider dans cette tâche, il existe plusieurs outils qui s’occuperont de lister l’ensemble des conventions qui ne seraient pas correctement suivies, le plus connu est pycodestyle.

The Zen of Python (PEP 20)

>>> 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!

Certains principes vont de soi - Beautiful is better then ugly -, d’autres sont des private jokes (Guido est néerlandophone). Dans l’ensemble, suivez ces principes les yeux fermés, et en cas de doute, revenez-y.

Vérification syntaxique et conventionnelle du code

Il existe plusieurs niveaux de vérifications de code :

  • Le premier niveau est assuré par pycodestyle, qui analyse votre code à la recherche d’erreurs de convention de style.
  • Le deuxième niveau est pyflakes. Il s’agit d’un simple footnote:[Ce n’est pas moi qui le dit, c’est la doc du projet] programme qui recherchera différents types d’erreurs parmi votre code source.
  • Le troisième niveau est Flake8, qui regroupe les deux premiers niveaux, en plus d’y ajouter flexibilité, extensions et une analyse de complexité de McCabe.
  • Le quatrième niveau sont PyLint et Ruff.

Ces deux derniers éléments sont les meilleurs ami de votre moi futur, un peu comme quand vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire le lendemain : ils rendront votre code soyeux et brillant, en posant des affirmations spécifiques. A vous de les traiter en corrigeant le code ou en apposant un tag indiquant que vous avez pris connaissance de la remarque, que vous en avez tenu compte, et que vous choisissez malgré tout de faire autrement.

Pour vous donner une idée, voici ce que cela pourrait donner avec un code pas très propre, qui ne sert pas à grand chose, et que nous allons faire passer parmi les différents niveaux de vérifications :

from datetime import datetime
"""Assigne la date du jour dans la variable ToD4y, puis l'imprime à l'écran"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)

pycodestyle

TBC

pyflakes

TBC

Flake8

Avec Flake8, nous obtiendrons ceci :

test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file

Nous trouvons des erreurs:

  • de conventions : le nombre de lignes qui séparent deux fonctions, le nombre d’espace après un opérateur, une ligne vide à la fin du fichier, … Ces erreurs n’en sont pas vraiment, elles indiquent juste de potentiels problèmes de communication si le code devait être lu ou compris par une autre personne.
  • de définition : une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug potentiel. Ne pas en tenir compte nuira sans doute à la santé de votre code (et risque de vous réveiller à cinq heures du mat’, quand votre application se prendra méchamment les pieds dans le tapis).

Pylint

L’étape d’après consiste à invoquer Pylint et Ruff. Eux sont directement moins conciliants :

Fenêtre de terminal
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10

En gros, j’ai programmé comme une grosse bouse anémique (et oui: le score d’évaluation du code permet d’aller en négatif). En détails, nous trouvons des problèmes liés :

  • au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
  • à des variables non définies (E0602)
  • de la documentation manquante (C0114, C0116)
  • de la redéfinition de variables (W0621).

Pour reprendre la documentation, chaque code possède sa signification :

  • C, pour toutes les vérifications liées aux conventions (nommage et mises en forme, que l’on a vues ci-dessus)
  • R, pour des propositions de refactoring
  • W, pour tout ce qui est en lien avec des avertissements
  • E pour les erreurs ou des bugs probablement présents dans le code
  • F pour les erreurs internes au fonctionnement de pylint, qui font que le traitement n’a pas pu aboutir.

Pylint propose également une option particulièrement efficace, qui prend le paramètre –errors-only, et qui n’affiche que les occurrences appartenant à la catégorie E. Connaissant ceci, il est extrêmement pratique d’intégrer cette option au niveau des processus d’intégration continue, puisque la présence d’une erreur détectée par Pylint implique généralement un correctif à appliquer.

Si nous souhaitons ignorer l’une de ces catégories, ce doit être fait explicitement: de cette manière, nous marquons notre approbation pour que pylint ignore consciemment un élément en particulier. Cet élément peut être :

  • Une ligne de code,
  • Un bloc de code : une fonction, une méthode, une classe, un module, …
  • Un projet entier,

Ruff

Lui, c’est le petit chouchou à la mode.

  • 10-100x faster than existing linters
  • Installable via pip
  • Python 3.11 compatibility
  • pyproject.toml support
  • Built-in caching, to avoid re-analyzing unchanged files
  • Autofix support, for automatic error correction (e.g., automatically remove unused imports)
  • Near-parity with the built-in Flake8 rule set
  • Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
  • First-party editor integrations for VS Code and more
  • Monorepo-friendly, with hierarchical and cascading configuration

Formatage de code

Nous avons parlé ci-dessous de style de codage pour Python (PEP8) , de style de rédaction pour la documentation (PEP257) , d’un vérificateur pour nous indiquer quels morceaux de code doivent absolument être revus, … Reste que ces tâches sont parfois (très) souvent fastidieuses: écrire un code propre et systématiquement cohérent est une tâche ardue. Heureusement, il existe plusieurs outils pour nous aider au niveau du formatage automatique. Même si elle n’est pas parfaite, la librairie Black arrive à un très bon compromis entre :

  • Clarté du code,
  • Facilité d’installation et d’intégration,
  • Résultat.

Le formattage qui sera proposé ne sera jamais idéal ni accepté par tout le monde - même Pylint arrivera parfois à râler -, mais il conviendra à la grande majorité des cas. Ceci a pour avantage de mettre tout le monde d’accord - un peu comme la commande go fmt pour le langage Go, qui est nativement inclue dans le langage et qui a pour objectif de mettre tout le monde d’accord.

By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.

Black makes code review faster by producing the smallest diffs possible. Blackened code looks the same regardless of the project you’re reading. Formatting becomes transparent after a while and you can focus on the content instead.

Traduit rapidement à partir de la langue de Batman :

“En utilisant Black, vous cédez le contrôle sur le formatage de votre code. En retour, Black vous fera gagner un max de temps, diminuera votre charge mentale et fera revenir l’être aimé”. Mais la partie réellement intéressante concerne le fait que “Tout code qui sera passé par Black aura la même forme, indépendamment du project sur lequel vous serez en train de travailler. L’étape de formatage deviendra transparente, et vous pourrez vous concentrer sur le contenu”.

Loggers

La configuration des loggers peut être relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore. Il est ainsi possible de définir des formattages, différents gestionnaires (handlers) et loggers distincts, en fonction de nos applications.

Sauf que comme nous l’avons vu lors du chapitre sur les 12 facteurs, il vaut mieux traiter l’ensemble des informations de notre application comme des flux d’évènements. Il n’est donc pas réellement nécessaire de beaucoup chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les StreamHandler, qui seront pris en charge par gunicorn. La configuration que nous allons utiliser est celle-ci :

  • Formattage: à définir - mais la variante suivante est complète, lisible et pratique: {levelname} {asctime} {module} {process:d} {thread:d} {message}
  • Handler: juste un, qui définit un StreamHandler, pour respecter les 12 facteurs.
  • Logger: pour celui-ci, nous avons besoin d’un niveau (level) et de savoir s’il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de propagate à True.

Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant :

import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')

Quelques exemples.

Typage statique

Nous vous disions ci-dessus que Python est un langage dynamique interprété. Concrètement, cela signifie que des erreurs qui auraient pu avoir été détectées lors de la phase de compilation, ne le sont pas avec Python.

Nous avons également vu que plusieurs linters permettent d’appliquer un premier niveau de vérification syntaxique.

Il existe une solution plus stricte à ceci, sous la forme de Mypy, qui peut vérifier une forme de typage statique de votre code source, grâce à une expressivité du code, basée sur des annotations.

Ces vérifications se présentent de la manière suivante :

from typing import List
def first_int_elem(l: List[int]) -> int:
return l[0] if l else None
if __name__ == "__main__":
print(first_int_elem([1, 2, 3]))
print(first_int_elem(['a', 'b', 'c']))

Est-ce que le code ci-dessous fonctionne correctement ? Oui :

>>> python mypy-test.py
1
a

Malgré que nos annotations déclarent une liste d’entiers, rien ne nous empêche de lui envoyer une liste de caractères, sans que cela ne lui pose de problèmes. La signature de notre fonction n’est donc pas cohérente avec son comportement.

Est-ce que Mypy va râler ? Oui, aussi. Non seulement nous retournons la valeur None si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous l’appelons avec une liste de caractères, alors que nous nous attendons à une liste d’entiers :

>>> mypy mypy-test.py
mypy-test.py:7: error: Incompatible return value type (got "Optional[int]", expected "int")
mypy-test.py:12: error: List item 0 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 1 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 2 has incompatible type "str"; expected "int"
Found 4 errors in 1 file (checked 1 source file)

Pour corriger ceci, nous devons :

  • Importer le type Optional et l’utiliser en sortie de notre fonction first_int_elem
  • Eviter de lui donner de mauvais paramètres ;-)²

Mypy demande une rigueur supplémentaire, qui peut s’avérer extrêmement utile sur des grosses bases de code ou avec de larges équipes, mais qui nécessite également un gros travail d’intégration, qui pourrait potentiellement ralentir le travail.

Introspection

def on_command_1():
print("Oune")
def on_command_2():
print("Dos")
def on_command_3():
print("Tresse")
def prrrt():
print("Won't be printed")

Tu peux retrouver les trois méthodes +on_command_*+‘ via la fonction ‘dir()‘:

>>> import module_chelou
>>> dir(module_chelou)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'on_command_1', 'on_command_2', 'on_command_3', 'prrrt']
>>> for function_name in [x for x in dir(module_chelou) if "on_command_" in x]:
... getattr(module_chelou, function_name)()
...
Oune
Dos
Tresse