14 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 en profondeur, notamment à cause de la richesse de ses paradigmes et de son écosystème.
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. 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. Python est notamment utilisé dans des frameworks web comme Django (que nous verrons plus loin), lui-même 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.
- Python est un langage dynamiquement typé : les variables n’ont pas de type explicite, mais chaque objet en a un. Le typage est fort - il n’y a pas de coercion implicite entre types incompatibles - mais peut être contourné volontairement ; une chaîne de caractères
xpeut 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), dont les évolutions sont régies par les les PEP, ou “
__Python Enhancement Proposal__”, qui peuvent volontairement briser la rétrocompatibilité - cf. Python 2 et Python 3.
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: 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, https://automatetheboringstuff.com/[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.
14.1 Guide de style (PEP 8)
Cette spécification 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, Readability 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.
14.2 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, quel que soit votre langage de prédilection.
14.3 Vérification syntaxique et conventionnelle du code
L’écosystème de la vérification de code a évolué. Si l’on trouvait autrefois une suite d’outils distincts, Ruff s’impose aujourd’hui comme la solution recommandée et ultime, remplaçant avantageusement les anciennes générations : pycodestyle se limitait aux conventions de style, pyflakes à la détection d’erreurs basiques et Flake8 tentait de réunir ces deux mondes avec une analyse de complexité. Ruff intègre désormais toutes ces fonctionnalités (et bien d’autres) en une seule commande, avec une rapidité d’exécution inégalée.
Bien que PyLint reste un outil puissant, Ruff est devenu le standard moderne pour la qualité du code Python.
Ces outils 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)14.3.1 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
$ uvx ruff check
F841 Local variable `today` is assigned to but never used
--> main.py:7:5
|
6 | def print_today(ToD4y):
7 | today = ToD4y
| ^^^^^
8 | print(ToD4y)
|
help: Remove assignment to unused variable `today`
F821 Undefined name `Get_Today`
--> main.py:14:9
|
13 | if __name__ == "__main__":
14 | t = Get_Today()
| ^^^^^^^^^
15 | print(t)
|
14.4 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 https://black.readthedocs.io/en/stable/[Black] arrive à un très bon compromis entre :
- Clarté du code,
- Facilité d’installation et d’intégration,
- Résultat.
Le formatage 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 https://pkg.go.dev/fmt[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**”.
14.5 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 depropagateàTrue.
Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant :
import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')https://docs.djangoproject.com/en/stable/topics/logging/#examples[Quelques exemples].
14.6 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 http://mypy-lang.org/[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
aMalgré 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
Optionalet l’utiliser en sortie de notre fonctionfirst_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.
14.7 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
Tresse14.8 Structures de données
Les principales structures de données sont :
- Les tuples, tableaux et listes,
- Les dictionnaires,
- Les namedtuples et dataclasses,
- Les classes.
14.8.1 Listes et tuples
Python distingue principalement les séquences muables et immuables :
Un tuple (t = tuple()``) ne présente ainsi que les méthodes count et index, là où une liste ou un tableau (l = []``), tout les deux représentés par [] sont des variables muables. Les méthodes accessibles à une instance de liste sont :
- ‘append’,
- ‘clear’,
- ‘copy’,
- ‘count’,
- ‘extend’,
- ‘index’,
- ‘insert’,
- ‘pop’,
- ‘remove’,
- ‘reverse’,
- ‘sort’
L’empreinte d’un tuple sera également plus faible qu’une liste : les tuples étant immuables, Python peut optimiser leur stockage et éviter certains contrôles à l’exécution. Choisissez en fonction de vos besoins.
14.8.2 Named tuples
Comme vu ci-dessus, vous pourriez avoir besoin d’un tuple lorsque vous avez besoin d’une empreinte mémoire plus faible qu’une liste, ou simplement pour retourner un ensemble défini de valeurs à partir d’une fonction. Django propose ainsi quelques fonctions comme le get_or_create(), qui retourne l’object construit ou récupéré (get), et s’il a dû être créé ou non. Plus concrètement, nous récupérons ainsi deux valeurs immuables, dont la seconde indique si l’objet retourné existait auparavant ou non.
from models import Library
object, created = Library.objects.get_or_create(title="The Subtle Art of Not Giving a F*ck")L’intérêt de ceci tient en un mot : simplicité. La simplicité peut cependant devenir un frein à la maintenabilité, raison pour laquelle un tuple peut nécessiter un brin de refactoring. La solution consiste alors à passer par un namedtuple, qui permet d’associer une nomenclature au niveau des propriétés de la structure, tout en gardant ses possibilités d’indexation.
>>> Point = namedtuple('Point', ['x', 'y', 'z'])
>>> p = Point(1, 2, 3)
>>> print(p.y)
214.8.3 Dictionnaires
Un dictionnaire ({}) correspond à une table de hash (hashmap) dans d’autres langages.
Il associe une clé à une valeur, au travers d’une table de correspondances. L’accès à chaque clé est ainsi rendu très rapide, au détour d’une éventuelle complexité à l’utilisation.
L’accès à une clé qui n’existe pas lèvera une exception de type KeyError :
>>> canevas = dict() # identique à canevas = {}
>>> canevas["a"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'a'Pour palier à ceci, nous pouvons utiliser la méthode get(), qui permet de spécifier une valeur par défaut si la clé venait à ne pas exister :
>>> canevas = {}
>>> canevas.get("a", "abracadabra")
'abracadabra'Une des méthodes les plus pratiques est la méthode setdefault, qui retourne la valeur de la clé si elle existe, et crée une nouvelle occurrence (avec une valeur par défaut) si elle n’existait pas encore :
>>> d = {}
>>> d.setdefault(1, "Yolo") # on cherche la clé "1"
'Yolo'
>>> d
{1: 'Yolo'}
>>> d.setdefault(1, "Yulu")
'Yolo'Ceci évite, dans une boucle, de vérifier si la clé existe déjà, et si pas, de la créer.
Pour des cas d’utilisation avancés, il existe également collections.defaultdict et collections.Counter.
14.8.4 Set et frozenset
Un set (ensemble) est une collection non ordonnée d’éléments uniques. Il est particulièrement utile lorsqu’on souhaite éliminer les doublons d’une séquence ou effectuer des opérations ensemblistes (union, intersection, différence, etc.).
fruits = {"pomme", "banane", "pomme", "orange"}
print(fruits)
{'orange', 'banane', 'pomme'}La structure est mutable, et dispose de méthodes telles que :
- add : ajoute un élément,
- remove : supprime un élément (ou discard, qui ne lève pas d’erreur si l’élément est absent),
- clear : vide l’ensemble,
- update : ajoute plusieurs éléments à la fois.
s = set()
s.add("pomme")
s.update(["banane", "kiwi"])
print(s)
{'kiwi', 'banane', 'pomme'}Les ensembles permettent aussi des opérations mathématiques efficaces :
|ouunion(): union,&ouintersection(): intersection,-oudifference(): différence,^ousymmetric_difference(): éléments présents dans un seul des deux ensembles.
a = {1, 2, 3}
b = {3, 4, 5}
a & b
{3}
a | b
{1, 2, 3, 4, 5}Si vous avez besoin d’un ensemble immuable, utilisez frozenset, qui permet les mêmes opérations que set, mais sans modification possible après création.
f = frozenset([1, 2, 3])
f | {3, 4}
frozenset({1, 2, 3, 4})
f.add(4)
AttributeError: 'frozenset' object has no attribute 'add'L’intérêt d’un frozenset apparaît notamment lorsque vous devez utiliser un ensemble comme clé de dictionnaire, ou comme élément dans un autre ensemble — ce qui nécessite l’immuabilité.
14.8.5 Classes
Une classe se concentre sur le comportement d’un élément :
from decimal import Decimal
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item: str, price: Decimal):
self.items.append({"item": item,"price": price})
def total(self) -> Decimal:
return sum(item["price"] for item in self.items)
cart = ShoppingCart()
cart.add_item("apple", Decimal("1.5"))
cart.add_item("banana", Decimal("2.0"))
print(f"${cart.total():.2f}")
$3.50==== Dataclasses
Les dataclasses permettent surtout d’éviter le boilerplate lié aux classes : constructeurs, méthodes, …
14.9 [source,python]
from dataclasses import dataclass from decimal import Decimal
(dataclass?) class Item: name: str price: Decimal
item = Item(name=“apple”, price=Decimal(“1.5”)) print(item) print(f”${item.price:.2f}“)
Item(name=‘apple’, price=Decimal(‘1.5’)) $1.50 —-
=== Compréhensions
Les comprehensions en Python permettent d’écrire du code plus concis et plus idiomatique. Elles ne servent pas uniquement aux listes, et permettent de simplifier certains blocs de code pour lesquels vous auriez eu besoin d’un itérateur (au moins) dans un autre langage.
14.10 [source,python,Génère une liste contenant le carré des nombres pairs]
squares = [x**2 for x in range(10) if x % 2 == 0]
14.11 [0, 4, 16, 36, 64]
14.12 [source,python,Correspondance entre un nombre et son cube]
cubes = {x: x**3 for x in range(6)}
{ 0: 0, 1: 1, 2: 8, 3: 27, 4: 64, 5: 125 } —-
14.13 [source,python]
words = [“hello”, “world”, “python”] unique_lengths = {len(workds) for word in words}
14.14 {5, 6}
14.15 [source,python,Applatissement d’une matrice à deux dimensions]
matrix = [[1, 2], [3, 4], [5, 6]] flattened = [num for row in matrix for num in row]
14.16 [1, 2, 3, 4, 5, 6]
=== Yield
Une astuce vraiment intéressante consiste à utiliser le mot-clé yield pour ne retourner la prochaine valeur que lorsqu’elle sera demandée ; l’empreinte mémoire est à nouveau très faible, puisqu’il n’y a jamais qu’un seul élément retourné.
TBC
=== Conclusions
En résumé, Python est un langage simple en apparence, mais riche et subtil dès qu’on y plonge plus profondément. En suivant quelques conventions, en s’appuyant sur les bons outils, et en restant fidèle à l’esprit du Zen, vous poserez une base solide pour écrire du code maintenable, élégant… et pythonique.