Site original : Sam & Max: Python, Django, Git et du cul
Avec de nombreuses distros Linux qui viennent avec Python 3 par défaut ainsi que Django et Pyramid qui annoncent bientôt ne plus supporter Python 2, il est temps de faire un point.
Python 3 est aujourd’hui majoritairement utilisé pour tout nouveau projet ou formation que j’ai pu rencontrer. Les plus importantes dépendances ont été portées ou possèdent une alternative. Six
et Python-future
permettent d’écrire facilement un code compatible avec les deux versions dans le pire des cas.
Nous sommes donc bien arrivés à destination. Il reste quelques bases de code encore coincées, la vie est injuste, mais globalement on est enfin au bout de la migration. Mais ça en a mis du temps !
Il y a de nombreuses raisons qui ont conduit à la lenteur de la migration de la communauté :
Mais je pense que la raison principale c’est le manque de motivation pour le faire. Il n’y a pas un gros sticker jaune fluo d’un truc genre “gagnez 30% de perfs en plus” que les devs adorent même si ça n’influence pas tant leur vie que ça. Mais les codeurs ne sont pas rationnels contrairement à ce qu’ils disent. Ils aiment les one-liners, c’est pour dire.
Pourtant, il y a des tas de choses excellentes en Python 3. Simplement:
Après 2 ans de Python 3 quasi-fulltime, je peux vous le dire, je ne veux plus coder en Python 2. Et on va voir pourquoi.
On a crié que c’était la raison principale. A mon avis c’est une erreur. Peu de gens peuvent vraiment voir ce que ça implique.
Mais en tant que formateur, voilà ce que je n’ai plus a expliquer:
# coding:
from codecs import open
et non pas juste open
.request.get().read() + 'é'
fait planter le programme. Et encode()
et decode()
.os.listdir()[0] + 'é'
fait afficher des trucs chelous.print(sql_row[0])
fait planter le programme ou affiche des trucs chelous. Ou les deux.Et je peux supprimer de chacun de mes fichiers:
u
ou les from __future__/codecs
encode
/decode
.Et toutes les APIS ont un paramètre encoding
, qui a pour valeur par défaut ‘UTF8′.
Des centaines d’ajustements ont été faits pour faciliter la gestion des erreurs et le debuggage. Meilleures exceptions, plus de vérifications, meilleurs messages d’erreur, etc.
Quelques exemples…
En Python 2:
>>> [1, 2, 3] < "abc" True |
En Python 3 TypeError: unorderable types: list() < str()
of course.
En Python 2, IOError
pour tout:
>>> open('/etc/postgresql/9.5/main/pg_hba.conf') Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: [Errno 13] Permission denied: '/etc/postgresql/9.5/main/pg_hba.conf' >>> open('/etc/postgresql/9.5/main/') Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: [Errno 21] Is a directory: '/etc/postgresql/9.5/main/' |
En Python 3, c'est bien plus facile à gérer dans un try
/except
ou à debugger:
>>> open('/etc/postgresql/9.5/main/pg_hba.conf') Traceback (most recent call last): File "<stdin>", line 1, in <module> PermissionError: [Errno 13] Permission denied: '/etc/postgresql/9.5/main/pg_hba.conf' >>> open('/etc/postgresql/9.5/main/') Traceback (most recent call last): File "<stdin>", line 1, in <module> IsADirectoryError: [Errno 21] Is a directory: '/etc/postgresql/9.5/main/' |
Les cascades d'exceptions en Python 3 sont très claires:
>>> try: ... open('/') # Erreur, c'est un dossier: ... except IOError: ... 1 / 0 # oui c'est stupide, c'est pour l'exemple ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IsADirectoryError: [Errno 21] Is a directory: '/' During handling of the above exception, another exception occurred: File "<stdin>", line 4, in <module> ZeroDivisionError: division by zero |
La même chose en Python 2:
Traceback (most recent call last): File "<stdin>", line 4, in <module> ZeroDivisionError: integer division or modulo by zero |
Bonne chance pour trouver l'erreur originale.
Plein de duplications ont été retirées.
En Python 2, un dev doit savoir la différence entre:
range()
et xrange()
map/filter
et itertools.imap/itertools.ifilter
dict.items/keys/values
, dict.iteritems/keys/values
et dict.viewitems/keys/values
open
et codecs.open
md5
vs hashlib.md5
getopt
, optparse
, argparse
sys
, os
ou shutil
?UserDict
ou dict
? UserList
ou list
?Sous peine de bugs ou de tuer ses perfs.
Certains bugs sont inévitables, et les modules csv
et re
sont par exemple pour toujours buggés en Python 2.
Faire un programme correct en Python 2 requière plus de code. Prenons l'exemple d'une suite de fichier qui contient des valeurs à récupérer sur chaque ligne, ou des commentaires (avec potentiellement des accents). Les fichiers font quelques centaines de méga, et je veux itérer sur leurs contenus.
Python 2:
# coding: utf8 from __future__ import unicode_literals, division import os import sys from math import log from codecs import open from glob import glob from itertools import imap # pour ne pas charger 300 Mo en mémoire d'un coup FS_ENCODING = sys.getfilesystemencoding() SIZE_SUFFIXES = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] def file_size(size): order = int(log(size, 2) / 10) if size else 0 # potential bug with log2 size = size / (1 << (order * 10)) return '{:.4g} {}'.format(size, suffixes[order]) def get_data(dir, *patterns, **kwargs): """ Charge les données des fichiers """ # keyword only args convert = kwargs.get('convert', int) encoding = kwargs.get('encoding', 'utf8') for p in patterns: for path in glob(os.path.join(dir, p)): if os.path.isfile(path): upath = path.decode(FS_ENCODING, error="replace") print 'Trouvé: ', upath, file_size(os.stat(path).st_size) with open(path, encoding=encoding, error="ignore") as f: # retirer les commentaires lines = (l for l in f if "#" not in l) for value in imap(convert, f): yield value |
C'est déjà pas mal. On gère les caractères non ASCII dans les fichiers et le nom des fichiers, on affiche tout proprement sur le terminal, on itère en lazy pour ne pas saturer la RAM... Un code assez chouette, et pour obtenir ce résultat dans d'autres langages vous auriez plus dégueu (ou plus buggé).
Python 3:
# wow, so much space, much less import from math import log2 # non buggy log2 from pathlib import Path # plus de os.path bullshit SIZE_SUFFIXES = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] def file_size(size): order = int(log2(size) / 10) if size else 0 size = size / (1 << (order * 10)) return f'{size:.4g} {suffixes[order]}' # fstring def get_data(dir, *patterns, convert=int, encoding="utf8"): # keyword only """ Charge les données des fichiers """ for p in patterns: for path in Path(dir).glob(p): if path.is_file(): print('Trouvé: ', path, file_size(path.stat().st_size)) with open(path, encoding=encoding, error="ignore") as f: # retirer les commentaires lines = (l for l in f if "#" not in l) yield from map(convert, lines) # déroulage automatique |
Oui, c'est tout.
Et ce n'est pas juste une question de taille du code. Notez tout ce que vous n'avez pas à savoir à l'avance pour que ça marche. Le code est plus lisible. La signature de la fonction mieux documentée. Et il marchera mieux sur une console Windows car le support a été amélioré en Python 3.
Et encore j'ai été sympa, je n'ai pas fait la gestion des erreurs de lecture des fichiers, sinon on en avait encore pour 3 ans.
Il y a des tas d'autres trucs.
L'unpacking généralisé:
>>> a, *rest, c = range(10) # récupérer la première et dernière valeur >>> foo(*bar1, *bar2) # tout passer en arg >>> {**dico1, **dico2} # fusionner deux dico :) |
Ou la POO simplifiée:
class FooFooFoo(object): ... class BarBarBar(FooFooFoo): def wololo(self): return super(BarBarBar, self).wololo() |
Devient:
class FooFooFoo: ... class BarBarBar(FooFooFoo): def wololo(self): return super().wololo() |
Python 3 est tout simplement plus simple, et plus expressif.
Evidement il y a plein de trucs qui n'intéresseront qu'une certaine catégorie de devs:
ipaddress.ip_address
pour parser les adresses IP.asyncio
pour faire de l'IO non blocante sans threads.enum
des enum de toutes sortes.functools.lru_cache
pour cacher le résultat de ses fonctions.@
pour multiplier des matrices avec numpy
.concurrent.futures
pour faire des pools non blocantes propres.statistics
stats performantes et correctes.tracemalloc
trouver les fuites de mémoire.faulthandler
gérer les crash du code C proprement.Si vous n'êtes pas concernés, ça n'est pas motivant. Mais si ça vous touche personnellement, c'est super cool.
Au passage, depuis la 3.6, Python 3 est enfin plus rapide que Python 2 pour la plupart des opérations :)
Toutes ces choses là s'accumulent. Un code plus court, plus lisible, plus facile à débugger, plus juste, plus performant. Par un tas de petits détails.
Alors oui, la migration ne va pas restaurer instantanément votre érection et vous faire perdre du poids. Mais sur le long terme, tout ça compte énormément.