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


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

  • Python 2 est un très bon langage. On ne répare pas ce qui marche.
  • Il y a beaucoup de code legacy en Python 2 et ça coûte cher de migrer.
  • La PSF a été trop gentille avec la communauté et l’a chouchoutée. En JS et Ruby ils ont dit “migrez ou allez vous faire foutre” et tout le monde a migré très vite.

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:

  • Elles ne sont pas sexy.
  • Elles ne se voient pas instantanément.
  • Personne n’en parle.

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:

  • Pourquoi un accent dans un commentaire fait planter le programme. Et # coding:
  • Pourquoi il faut faire from codecs import open et non pas juste open.
  • Pourquoirequest.get().read() + 'é' fait planter le programme. Et encode() et decode().
  • Pourquoi os.listdir()[0] + 'é' fait afficher des trucs chelous.
  • Pourquoi 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:

  • Tous les u ou les from __future__/codecs
  • Les en-tête d’encoding.
  • La moitié des encode/decode.

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:

  • 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
  • Et md5 vs hashlib.md5
  • getopt, optparse, argparse
  • Cette manipulation de fichier se fait avec sys, os ou shutil ?
  • On hérite de 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.

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:

  • 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.
  • type hints pour vérifier la validité de son code.
  • l'opérateur @ 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 :)

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.

31 thoughts on “Tout ce qui fait que Python 3 est meilleur que Python 2

  • desfrenes

    “Django et Pyramid qui annoncent bientôt ne plus supporter Python 3”

    c’est pas python 2 plutôt ?

  • Emmanuel

    Oooops : au tout début de l’article “qui annoncent bientôt ne plus supporter Python 3” —> c’est 2 ;-)

  • asshole

    Tout ce qui fait que je suis passé directement de Python 2 à Go :

    gestion de la concurrence intégrée dans le language,
    distribution de programmes facile,
    rapidité de compilation et d’execution,
    vérification de formatage par défaut,
    gestion des tests par défaut,
    gestion de la doc par défaut.

    J’en pouvais plus de passer 3 heures à éplucher 12 docs différentes qui se contredisent pour distribuer un programme…

    Je n’aime pas l’évolution que Python 3 prend…

    L’asynchrone ne me convainc pas depuis que j’ai goûté aux threads légers de Go.

    Bref, j’utilise toujours Python pour des scripts d’admin rapides à écrire mais plus pour des projets entiers…Pourtant il y a avait l’exemple de Perl…On ne casse pas une communauté sinon c’est la fin…et j’ai pas que cela à branler de réecrire mes progs d’il y a 10 ans et d’aller à la pêche aux modules voir s’ils sont v2 ou v3…mais je vous rassure, je trouve qu’il y a pire : le Javascript ;)

    • Sam Post author

      @asshole : a moins d’écrire un serveur, Go n’a quasiment aucun avantage sur Python. Analyser des données, manipuler du texte, faire des script, gérer des erreurs, faire un site web… Python est toujours plus facile à utiliser pour cela. Oui la distribution est meilleur, mais vu qu’on fait rarement des programmes avec GUI avec au final ça n’apporte pas grand chose. Quitte à apprendre un langage bas niveau et se faire chier, autant prendre rust.

  • Sam Post author

    @gordon:

    Oui c’est une des raisons pour lesquelles je ne recommande pas son livre. Si certains points sont intéressant, il y a beaucoup de mauvaise foi. Particulièrement, Python 3 est immensément meilleur pour les débutants. C’est justement un des points principaux d’attraction. Dire le contraire est soit un mensonge, soit de l’incompétence.

    @david: ouai j’ai bu

  • blackmoor

    Typos:

    – “Et toutes les APIS ont un paramètres encoding” –> “un paramètre”

    – “Des centaines d’ajustements on été fait” –> “ont été”

    – “Les fichiers font quelques centaines de méga, et jeux veux itérer sur leurs contenus.” –> “et je veux”

    – “Et encore j’ai été sympas” –> “sympa”

    – “la migration ne va pas réstaurer instantannément votre erection et vous faire perdre du poids” –> restaurer instantanément votre érection

    Est-ce une typo ? “En Python un dev doit savoir la différence entre:” –> “En Python 2”

    HS: Quitte à poster un message, un grand merci à toi Sam pour tout ces articles super intéressant (merci aussi à Max, mais j’ai l’impression de ne pas le lire souvent)

    Question: je ne code pas vraiment en python même si j’ai été amené à en faire un tout petit peu. N’est-ce pas gênant de n’apprendre que python3 quand on commence ? Ne devrait-on pas d’abord galérer un peu sur le 2 histoire de mieux comprendre ?

  • Sam Post author

    @blackmoor : merci. Non plus maintenant, tu peux oublier Python 2. Ca ne servira pas assez. Si un jour par malchance tu dois lire ou porter une code Python 2, il sera temps de t’y pencher, mais seulement à ce moment là. Ceci dit juste lire un code Python 2 est facile si on connait Python 3. Le langage n’est pas fondamentalement différent.

  • Splint'

    Ca fait plaisir ces nouveaux posts !

    Je partage clairement ton point de vue sur Python 3, surtout depuis l’arrivee 3.5

    Typos reperees dans la fonction get_data de l’exemple Python3

    * une parenthèse en trop après le glob(p)

    * if path.isfile(): => if path.is_file():

    * path.stat.st_size => path.stat().st_size

  • Zesk06

    Mais moi, je bosse sous CentOS 6.5, et sous CentOS 6.5 c’est encore python 2.6 par défaut … oui 2.6

    On peut s’en sortir en installant les software collections mais du coup il faut envoyer 46 commandes avant de pouvoir taper du python3.

    bref c’est pas gagné

  • entwanne

    Reste à ce que les distributions Linux à la traîne finissent par accepter qu’aujourd’hui, python == python3…

  • cocksucker

    @Sam

    Avoue, tu voulais juste vérifier qu’on avait bien tous migré vers Python 3 ? ;), une forme d’audit informel en mode shadow ;)

  • Sam Post author

    @Zesk06: ouai comme je le disais, le monte est injuste.

    Mes palliatifs sont :

    • Pythonz (4 commandes pour installer Python 2.6 https://github.com/saghul/pythonz).
    • nuikta. J’ai Python2.6 + headers + nuikta sur ma machine et je fais nuitka –standalone –python-version=2.6 mon_script.py et ça me sort un mon_script.dist/mon_script.exe stand alone qui marche sur tous les vieux serveurs sans rien installer. Mais le dossier fait 20Mo pour un hello world :)
  • Abject

    Hey super !!! De noveaux articles sur Sam & Max venez tous !!!!!!!!!!!!!!

    Enfin un point exhaustif (ou presque) des différences (oserais-je dire des avantages) de python 3 par rapport à Python 2…

    Ben moi je dois faire des code Python 3 sur Cent OS 7 mais j’ai pas la possibilité d’avoir toutes les lib dispos pour Python 3… –> on fait du backport des lib Python3 vers Python2 pour qu’au final on puisse faire du Python3…

    Vivement 2020 et la fin officielle de Python 2 !!!!

  • fero14041

    Pu*ain le con (c’est moi), j’avais défacé l’article avec une balise “code” mal fermée… Mes plus plates excuses à tous les lecteurs de passage!

  • Réchèr

    Yep !

    Sans oublier les imports relatifs et absolus, qui clarifient bien les choses, et le fameux “1 / 2 = 0” qui a du dérouter bon nombre de néophytes.

    Si vous êtes un homme, coder en python 3 multiplie par 1.5 la quantité de sperme lors d’une éjaculation.

    Si vous êtes une femme, la densité de corpuscule de Krause sur le clitoris est multipliée par 1.5 également.

    Vivement le python 4 !

  • mzk

    Sure! Je me suis littéralement plongé dans la programmation Python à partir de sa version 3 (et plus précisément la version 3.4), un peu parhasard, alors que ça faisait des années que je lorgnais sur ce langage. Mais la plupart des tutos et autres publications tournaient essentiellement sur la version 2 – qui rebute plus d’un débutant !

    C’est génial que cette version apporte nativement des tas d’outils assez puissants, que ses performances s’améliorent et dépassent désormais la version 2 et que ça ouvre des perspectives aussi larges sur le développement (applicatif, web, etc.)

    Mais plusieurs questions :

    – pour Fabric bloque encore sur la version 2.7 de python et bute sur le portage en version 3 ? Manque de dev ou bien ??

    – pourquoi rust plutôt que Go ? Je suis en train de me former sur Golang et j’avoue que le passage par Python 3 et Perl m’aide considérablement à comprendre aussi bien la syntaxe que la logique de ce langage…

    – Que va devenir Python dès qu’il devra franchir la barre du 3.9 ? Car à la vitesse où ça passe d’une souche à l’autre, c’est pour très bientôt, non ?

    En tout cas, super vos articles… Vivement le prochain !

  • Sam Post author

    1 – Faut demander à Fabric.

    2 – Parce que Go est “ni bas ni haut niveau”. Si on veut un truc productif, on utilise Python. Si on veut du control, on utilise rust (ou c, ou c++). Go n’a d’interêt que pour sa gestion de la concurrence, mais alors autant utiliser erlang. C’est un langage qui n’est pas bon partout, mais pas le meilleur dans sa spécialité non plus. Pas très avantageux à apprendre.

    3 – Python suit semver. Après la 3.9, c’est donc 3.10, 3.11, etc. Mais même au passage de la 4.0, Guido a clairement spécifié qu’il n’y aura pas la même politique que pour la 3.0 car ça avait été trop dur à gérer.

  • deronnax

    @mzk: le problème est que le mec de Fabric, Jeb Forcier, semble être un gros con, en tout cas agit comme tel. Après avoir promis Fabric2, compatible python 3 vers 2014, le mec a ensuite plus donné d’informations. développe le projet sur un dépot privé (c’est ce qu’il affirme, personne n’a pu vérifier), et ferme sans explications tous les tickets Github qui demandent des informations sur le sujet ou proposent de l’aide [1]. C’est d’autant plus inacceptable que des mecs ont porté Fabric sur Python 3 en utilisant 2to3 et c’était facile [2]. Bref, un fork compatible Python3 a été fait et il semble très bien fonctionner : https://github.com/mathiasertl/fabric/

    [1] https://github.com/fabric/fabric/issues/1417 https://github.com/fabric/fabric/issues/565 https://github.com/fabric/fabric/issues/1050#issuecomment-140577614

    [2] https://github.com/fabric/fabric/issues/1424#issuecomment-179552669

  • Sam Post author

    C’est pas être un con ça, c’est pas avoir le temps et être dépassé par son projet.

  • Abject

    Je suis récément tombé sur ce genre de press anti-python : https://learnpythonthehardway.org/book/nopython3.html !!!

    J’avoue que je ne sais pas quoi en penser certain argument sont ou semble pertinent mais je n’ai pas le recul (ou la profondeur) pour savoir en tirer parti… (l’auter non plus d’ailleurs).

    Qu’en pensez-vous ? Loin de moi l’idée de troller c’est un sujet sensible surtout vu la manière dont l’article est rédigé mais je shouaiterais savoir un petit peu ce que vous en pensez tous amis Pythoniste !

  • f4b1

    En tout cas, j’adore les débats qu’a provoqué cette nouvelle version, c’est passionnant !

  • Sam Post author

    @Abject : essentiellement du FUD. La question python 2/3 a été largement débattue, les bons et mauvais points sont listés, connus et traités. Faut passer à autre chose.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.