python 2 – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Tout ce qui fait que Python 3 est meilleur que Python 2 http://sametmax.com/tout-ce-qui-fait-que-python-3-est-meilleur-que-python-2/ http://sametmax.com/tout-ce-qui-fait-que-python-3-est-meilleur-que-python-2/#comments Thu, 26 Jan 2017 14:17:40 +0000 http://sametmax.com/?p=22144 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 "", line 1, in 
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 "", line 1, in 
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 "", line 1, in 
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 "", line 1, in 
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 "", line 2, in 
IsADirectoryError: [Errno 21] Is a directory: '/' 

During handling of the above exception, another exception occurred:

  File "", line 4, in 
ZeroDivisionError: division by zero

La même chose en Python 2:

Traceback (most recent call last):
  File "", line 4, in 
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.

]]>
http://sametmax.com/tout-ce-qui-fait-que-python-3-est-meilleur-que-python-2/feed/ 31 22144
Gérer plusieurs versions de Python avec “py” sous Windows http://sametmax.com/gerer-plusieurs-versions-de-python-avec-py-sous-windows/ http://sametmax.com/gerer-plusieurs-versions-de-python-avec-py-sous-windows/#comments Sat, 10 Jan 2015 09:10:27 +0000 http://sametmax.com/?p=15699 py sous Windows, qui permet de choisir quelle version de Python on lance.]]> Il est courant de vouloir installer plusieurs versions de Python. Pour faire des tests, pour s’assurer que son code est portable, pour utiliser des libs qui marchent uniquement sur une des versions, etc.

Sous Linux, c’est facile : chaque interpretteur est préfixé. Par exemple, sous Ubuntu, si je veux utiliser Python 3.4, je l’installe :

sudo apt-get install python3.4

Et si je lance python, ça me lance la 2.7 car c’est celle de base. Mais si je lance python3.4, ça me lance bien la 3.4.

Au final, on finit par utiliser des environnements virtuels qui isolent des versions de Python particulières. Mais ça ne retire pas l’envie de pouvoir choisir sa version de Python au niveau du système, ce qui reste difficile à faire sous Windows.

Or, depuis la version 3.3, l’installeur pour cet OS de Python installe la commande py, qui permet de choisir quelle version de Python on lance.

Dans un terminal

Si dans une console vous faites :

py

Il lancera le shell de votre installation Python 2.x la plus récente.

Si vous faites :

py script.py

Il exécutera le script avec votre installation Python 2.x la plus récente.

Mais vous pouvez passez un flag -version pour forcer une version de Python. Lancer le shell Python avec la 3.3 :

py -3.3

Lancer un script avec la 3.4 :

py -3.4 script.py

Cela suppose que vous avez la 3.3 et la 3.4, installés, évidement.

La syntaxe est surprenante. J’aurais pensé qu’ils mettraient un truc du genre py -i 3.4 mais non, c’est direct -numero.

La commande py accepte aussi les paramètres qu’on passerait normalement à la commande python, et notament l’option -m module, qui permet de lancer un module en particulier.

C’est pratique pour lancer ipython ou pip avec une version particulière. Par exemple pour installer autobahn uniquement pour la version 3.4 et donc utiliser asyncio :

py -3.4 -m pip install autobahn

Bang !

La commande py reconnait également la ligne shebang, cette syntaxe unix qui dit quel interpretteur utiliser. Si vous mettez sur la première ligne de votre script :

#! python3.4

Alors :

py script.py

Invoquera python 3.4.

La commande est capable de se débrouiller avec les chemins Unix, afin de garder la portabilité. Donc si vous faites :

#! /usr/bin/env python2.7

Alors :

py script.py

Va ignorer le debut de la ligne, et prendre l’installation locale de Python 2.7 pour lancer le script.

On click

Si vous installez la version Python 3.3 ou 3.4 en premier, les fichiers .py seront associés à la commande py. Donc si vous cliquez sur un script Python avec une ligne shebang, la bonne version sera lancée.

Mais si vous avez installé Python 2.x avant, il est possible que vos fichiers .py soient encore associés directement à la commande python ordinnaire.

Pour changer cela, faites un clic droit sur un fichier .py, modifiez le programme qui ouvre ce fichier et faites le pointer sur "C:\Windows\py.exe".

]]>
http://sametmax.com/gerer-plusieurs-versions-de-python-avec-py-sous-windows/feed/ 7 15699
Ca y est, on peut coder en Python 3 http://sametmax.com/ca-y-est-on-peut-coder-en-python-3/ http://sametmax.com/ca-y-est-on-peut-coder-en-python-3/#comments Mon, 17 Mar 2014 11:29:20 +0000 http://sametmax.com/?p=9790 Voici tout ce dont vous pouvez profiter avec Python 3 que vous ne pouvez pas faire avec Python 2.]]> Python 3.4 vient de sortir, et avec cette release, je peux enfin recommander aux gens de se mettre à Python 3 plutôt que Python 2. Tout simplement parce que maintenant, en plus d’avoir énormément de libs qui ont migré, on a l’expérience de la bestiole. C’est stable, les perfs sont bonnes, la conversion de code d’une version à l’autre est bien documentée et maitrisée.

Et surtout, la 3.3 et la 3.4 viennent avec tout un tas des goodies super green.

Voici tout ce dont vous pouvez profiter avec Python 3 que vous ne pouvez pas faire avec Python 2.

Des classes classes

Les classes sont des new type classes en Python 3. Pas besoin d’hériter d’object:

# Au revoir !
class TouteMoche(object):
    pass

# Bonjour !
class PropreEtNette:
    pass

Les metaclasses ne se définissent plus comme un attribut spécial :

# Au revoir !
class TouteMoche(object):
    __metaclass__ = UnTruc

# Bonjour !
class PropreEtNette(metaclass=Untruc):
    pass

Super Super()

Franchement, qui se souvient de la syntaxe pour appeler proprement la méthode parente d’une classe ?

# Au revoir !
class FranchementHein(object):

    def __init__(self):
        super(FranchementHein, self).__init__()

# Bonjour !
class FranchementHein:

    def __init__(self):
        super().__init__()

Meta meta programmation

Le module inspect permet maintenant de récupérer des infos très très précises sur la signature des fonctions :

    >>> from inspect import signature
    >>> def foo(a, *, b:int, **kwargs):
    ...     pass

    >>> sig = signature(foo)

    >>> str(sig)
    '(a, *, b:int, **kwargs)'

    >>> str(sig.parameters['b'])
    'b:int'

    >>> sig.parameters['b'].annotation
    

Et comme vous pouvez le voir, les annotations sont de la partie.

Les classes ont également un nouvel attribut __qualname__, qui, comme on peut s’y attendre, est le qualified name de la classe :

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

Ca marche aussi pour les fonctions.

Enfin pour les gars qui sont vraiment tordus, on a une nouvelle méthode pour les metaclasses, __locallookup__, qui permet d’influencer le MRO à la volée.

Un yield qui yield

Bon, mon titre ne veut rien dire, c’était pour la continuité.

yield a maintenant un petit frère, yield from, qui permet de déléguer l’itération à un itérable sous jacent :

def generateur():
    yield from "123"
    yield from (str(x) for x in range(4, 7))
    yield from ("7", "8", "9")

for i in generateur():
    print(i)

## 1
## 2
## 3
## 4
## 5
## 6
## 7
## 8
## 9

Là ce n’est pas très utile, mais sur des générateurs complexes, c’est sympa. Surtout que yield from passe aussi les valeur avec send(), fait remonter les exceptions proprement et est détecté par les events loops du module asyncio, dont on parlera plus tard.

Batteries included, with charger

Pip et virtualenv sont livrés avec la 3.4 ! Rien de plus à faire, rien à installer ! Enfin !

Malheureusement pas virtualenv wrapper, mais c’est déjà pas mal.

Un nouveau format, le wheel, permet de faire des paquets binaires qui n’auront pas besoin d’être compilés sur la machine cible. Ca veut dire bientôt du numpy / scipy sans avoir à compiler quoi que ce soit. C’est un remplacement du egg plus simple et sans les problèmes de compatibilités. Il ne vise pas la distribution de projets Python standalones, comme le egg qui a notamment eu le problème de vouloir tout faire en même temps.

Pour ce dernier, le pyz est en discussion.

Enfin les scripts Python supportent maintenant l’équivalent du shebang, mais sous Windows. Ceci vient avec l’introduction de la commande py qui permet de lancer toutes les commandes Python (pip, venv, etc.) pour une version de Python installée spécifique.

Ah, non, pas “enfin”, j’ai oublié un truc :

python -X faulthandler votre_script.py vous permet maintenant d’obtenir une stacktrace sur les scripts utilisant ctypes \o/. Et le module tracemalloc permet d’enquêter sur l’allocation mémoire.

Tout un tas de trucs que les codeurs Java vont adorer

Il y a des Enums, et ça vient sous toutes les formes :

from enum import Enum, IntEnum

# La version de feignasse :
Animal = Enum('ant bee cat dog')

# La version 'anal retentive'
class Animal(IntEnum):
     ant = 1
     bee = 2

# et tout ce qu'il faut entre les deux pour les centristes

Pour les nostalgiques de l’overloading, on peut maintenant spécifier que le code d’une fonction est différent selon le type des arguments qui lui sont passés :

from functools import singledispatch

@singledispatch
def fun(arg):
    print('Comportement par défaut, teddy')

@fun.register(int)
def _(arg):
    print("Tu m'as passé un int, jonhy !")

class Cacatoes:
    pass

@fun.register(Cacatoes)
def _(arg):
    print("Tu m'as passé un Cacatoes, billy !")


fun('Do')
fun([])
fun(1)
fun(Cacatoes)

## Comportement par défaut, teddy
## Comportement par défaut, teddy
## Tu m'as passé un int, jonhy !
## Tu m'as passé un Cacatoes, billy !

Et puis le module abc pour faire des classes abstraites, même si ça a été backporté en Python 2.7, donc un peu hors sujet.

Les built-in ont un peu changé

Pour le texte, c’est de l’unicode partout, avec utf8 par défaut pour les conversions. Ca veut dire plus de u devant les chaînes de caractère, plus de déclaration de l’encoding en haut des fichiers de code. Moins de decode / encode.

Mais ça veut dire aussi que open a besoin obligatoirement d’un paramètre encoding, dont la valeur par défaut est bien entendu utf8.

Attention cependant, le built-in bytes n’est pas l’exacte équivalent du type str en Python 2 puisqu’il ne possède plus certaines méthodes de manipulation de texte comme replace:

>>> "Trololo".replace('o', 'a')
'Tralala'
>>> b"Trololo".replace('o', 'a')
Traceback (most recent call last):
  File "", line 1, in 
    b"Trololo".replace('o', 'a')
TypeError: expected an object with the buffer interface

C’est un peu chiant quand on manipule des protocoles binaires, et les mecs de mercurial ont un peu gueulé. Donc il est possible que ça change. J’ai mis beaucoup de fois le mot “peu” dans cet article, ce qui est stylistiquement très laid. Mais je suis trop paresseux pour éditer cet article qui fait maintenant 3 km.

Pour les nombres, on n’a plus à se soucier du type long, qui n’existe plus. / est maintenant la division ordinaire et // la division entière.

Pour les fonctions built-in, pas mal de changements avec bien entendu, print() qui devient une fonction, mais aussi toute ce qui est zip, map, etc, qui retournent des générateurs au lieu de listes, tout comme dict.items et consorts. Ah oui, et import est maintenant absolu par défaut.

Quelques libs en plus

Marre du module os ? pathlib permet de donner un petit goût d’objet à vos manipulations de FS, comme path.py. Ca reste moins bien, mais c’est mieux que rien.

Raz-le bol de recoder la fonction moyenne, médiane, etc ? Le module statistics a été ajouté pour ça.

Enfin, le fameux module mock, qui permet de simuler tout un tas de trucs sans tout casser :

>>> from mock import patch
>>> with patch.object(os, 'listdir', return_value=['file2.txt', 'file2.text']):
    print(os.listdir('/etc/'))
...
[u'file2.txt', u'file2.text']
>>> os.listdir('/etc/')[:2]
[u'environment', u'hosts.allow']

Tout est bien rangé

Les fichiers bytecode Python sont maintenant tous groupés dans un dossier appelé __pycache__. Finis les .pyc qui trainent partout. Et en plus prefixés de l’architecture avec laquelle ils ont été générés. Toujours utile.

En prime pas mal de noms ont été normalisés : tous les modules sont maintenant en minuscule, tous les modules liés à IO sont groupés dans le module io, urllib 1 et 2 ont été mergés, et l’arborescence des exceptions pour les erreurs d’IO a maintenant beaucoup plus de sens :

# Au revoir !
from errno import ENOENT, EACCES, EPERM

try:
    with open("document.txt") as f:
        content = f.read()
except IOError as err:
    if err.errno == ENOENT:
        print("document.txt file is missing")
    elif err.errno in (EACCES, EPERM):
        print("You are not allowed to read document.txt")
    else:
        raise

# Bonjour !

try:
    with open("document.txt") as f:
        content = f.read()
except FileNotFoundError:
    print("document.txt file is missing")
except PermissionError:
    print("You are not allowed to read document.txt")

Asyncio, la prog asynchrone rebootée

Une des raisons pour laquelle je tape sur javascript aussi fort, c’est aussi la jalousie. Ils ont tous les derniers joujoux asynchrones hi-tech, meh !

Avec Python on était obligé d’installer tornado ou twisted pour ça, et c’était un peu dommage :)

Avec la 3.4, la prog asynchrone fait peau neauve (adieu l’horrible module asyncore) et propose de la prog asynchrone plus simple, plus légère, plus facilement intégrable : asyncio.

Si vous lisez ce PEP, ça ne va pas vous parler, alors je vais potasser tout ça et faire un petit article.

Mon seul regret avec asyncio, c’est que c’est un module uniquement bas niveau, alors que tulip, le prototype de la lib, contenait des mobiles haut niveau comme par exemple un client http bien foutu. Du coup ça a donné ce genre de discussion sur le blog, avec des gens qui ne comprenaient pas que je m’attendais à un peu de haut niveau et eux qui ne voyaient que le bas, et donc s’attardaient sur de la sémentique d’implémentation de la prog non blocante.

En conclusion

Le code Python 3 est plus court, plus cohérent, avec moins de surprises, moins de dépendances externes et plus de flexibilité. Des comportements par défaut sains un peu partout, des outils en plus, et comme d’habitude vous n’en utiliserez pas la moitié tellement il y a à faire.

Python 3.4 est vraiment un très bon cru, honnêtement, avec toutes ces améliorations, le langage parvient à se maintenir au niveau des petits jeunes et de leur hype, sans sacrifier sa solidité et sa cohérence.

C’est pour ça qu’on a attendu 5 ans. C’est pour ça que la migration a été lente et prudente.

Parce que la communauté Python fait vraiment les choses bien.

Les exemples du blogs seront donc à partir de maintenant en Python 3.

]]>
http://sametmax.com/ca-y-est-on-peut-coder-en-python-3/feed/ 38 9790