PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Tout ce qui fait que Python 3 est meilleur que Python 2 31

jeudi 26 janvier 2017 à 15:17

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.

Unicode, mon bel unicode

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:

Et je peux supprimer de chacun de mes fichiers:

Et toutes les APIS ont un paramètre encoding, qui a pour valeur par défaut ‘UTF8′.

Debuggage for the win

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:

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.

Good bye boiler plate

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.

Bonus

Evidement il y a plein de trucs qui n'intéresseront qu'une certaine catégorie de devs:

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 :)

Pour finir...

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.