Sortie de Python 3.5

Tour d'horizon de la dernière version du célèbre langage de programmation

Une nouvelle version du langage Python (et par conséquent de son implémentation principale CPython) sort aujourd'hui. Estampillée 3.5, cette version fait logiquement suite à la version 3.4 parue il y a un an et demi1. Tandis que cette dernière apportait principalement des ajouts dans la bibliothèque standard (asyncio, enum, ensurepip, pathlib, etc.), les nouveautés les plus visibles de la version 3.5 concernent des changements syntaxiques avec deux nouveaux mot-clés, un nouvel opérateur binaire, la généralisation de l'unpacking et la standardisation des annotations de fonctions.

Cette version 3.5 sera certainement perçue par les Pythonistes comme celle qui aura introduit le plus de changements au langage depuis Python 3.0. En effet, nous allons découvrir ensemble qu'avec cette nouvelle version, Python achève d'inclure dans le langage le support d'un paradigme de programmation moderne, revenu au goût du jour avec l'éclosion de Node.js : la programmation asynchrone.


  1. Le 16 Mars 2014 pour être précis. 

TL;DR - Résumé des principales nouveautés

Les plus pressés peuvent profiter du court résumé des principales nouveautés suivant. Chacune sera reprise dans la suite de l'article, en la détaillant et explicitant les concepts sous-jacents.

  • PEP 448 : les opérations d'unpacking sont généralisées et peuvent maintenant être combinées et utilisées plusieurs fois dans un appel de fonction.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    >>> def spam(a, b, c, d, e, g, h, i, j):
    ...    return a + b + c + d + e + g + h + i + j
    ...
    >>> l1 = (1, *[2])
    >>> l1
    (1, 2)
    >>> d1 = {"j": 9, **{"i": 8}}
    >>> d1
    {"j": 9, "i": 8}
    >>> spam(*l1, 3, *(4, 5), g=6, **d1, **{'h':7})
    45    # 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9
    

  • PEP 465 : l'opérateur binaire @ est introduit pour gérer la multiplication matricielle et permet d'améliorer la lisibilité d'expressions mathématiques. Par exemple, l'équation $A \times B^T - C^{-1}$ correspondrait au code suivant avec numpy.

    1
    A @ B.T - inv(C)
    

  • PEP 492 : les coroutines deviennent une construction spécifique du langage, avec l'introduction des mots-clés async et await. L'objectif étant de compléter le support de la programmation asynchrone dans Python, ils permettent d'écrire des coroutines utilisables avec asyncio de façon similaire à des fonctions Python synchrones classiques. Par exemple :

    1
    2
    3
    4
    5
    6
    7
    8
    async def fetch_page(url, filename):
        # L'appel à aiohttp.request est asynchrone
        response = await aiohttp.request('GET', url)
        # Ici, on a récupéré le résultat de la requête
        assert response.status == 200
        async with aiofiles.open(filename, mode='wb') as fd:
            async for chunk in response.content.read_chunk(chunk_size):
                await fd.write(chunk)
    

  • PEP 484 : les annotations apposables sur les paramètres et la valeur de retour des fonctions et méthodes sont maintenant standardisées et ne devraient servir qu'à préciser le type de ces éléments. Les annotations ne sont toujours pas utilisées par l'interpréteur et cette PEP n'est constituée que de conventions.

    1
    2
    def bonjour(nom: str) -> str:
        return 'Zestueusement ' + nom
    

Principales nouveautés

Unpacking généralisé – PEP 448

L'unpacking est une opération permetant de séparer un itérable en plusieurs variables :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> l = (1, 2, 3, 4)
>>> a, b, *c = l
>>> a
1
>>> b
2
>>> c
(3, 4)
>>> def f(a, b):
...    return a + b, a - b    # f retourne un tuple
...
>>> somme, diff = f(5, 3)
>>> somme
8    # 5 + 3
>>> diff
2    # 5 - 3

Il est alors possible de passer un itérable comme argument de fonction comme si son contenu était passé élément par élément grâce à l'opérateur * ou de passer un dictionnaire comme arguments nommés avec l'opérateur ** :

1
2
3
4
5
6
7
8
9
>>> def spam(a, b):
...    return a + b
...
>>> l = (1, 2)
>>> spam(*l)
3    # 1 + 2
>>> d = {"b": 2, "a": 3}
>>> spam(**d)
5    # 3 + 2

Cette fonctionnalité restait jusqu'à maintenant limitée et les conditions d'utilisation très strictes. Deux de ces contraintes ont été levées…

Support de l'unpacking dans les déclarations d'itérables

Lorsque vous souhaitez définir un tuple, list, set ou dict littéral, il est maintenant possible d'utiliser l'unpacking.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Python 3.4
>>> tuple(range(4)) + (4,)
(0, 1, 2, 3, 4)
# Python 3.5
>>> (*range(4), 4)
(0, 1, 2, 3, 4)

# Python 3.4
>>> list(range(4)) + [4]
[0, 1, 2, 3, 4]
# Python 3.5
>>> [*range(4), 4]
[0, 1, 2, 3, 4]

# Python 3.4
>>> set(range(4)) + {4}
set(0, 1, 2, 3, 4)
# Python 3.5
>>> {*range(4), 4}
set(0, 1, 2, 3, 4)

# Python 3.4
>>> d = {'x': 1}
>>> d.update({'y': 2})
>>> d
{'x': 1, 'y': 2}
# Python 3.5
>>> {'x': 1, **{'y': 2}}
{'x': 1, 'y': 2}

# Python 3.4
>>> combinaison = long_dict.copy()
>>> combination.update({'b': 2})
# Python 3.5
>>> combinaison = {**long_dict, 'b': 2}

Notamment, il devient maintenant facile de sommer des itérables pour en former un autre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> l1 = (1, 2)
>>> l2 = [3, 4]
>>> l3 = range(5, 7)

# Python 3.5
>>> combinaison = [*l1, *l2, *l3, 7]
>>> combinaison
[1, 2, 3, 4, 5, 6, 7]

# Python 3.4
>>> combinaison = list(l1) + list(l2) + list(l3) + [7]
>>> combinaison
[1, 2, 3, 4, 5, 6, 7]

Cette dernière généralisation permet ainsi d'apporter une symétrie par rapport aux possibilités précédentes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Possible en Python 3.4 et 3.5
>>> elems = [1, 2, 3, 4]
>>> fst, *other, lst = elems
>>> fst
1
>>> other
[2, 3]
>>> lst
4

# Possible uniquement en Python 3.5
>>> fst = 1
>>> other = [2, 3]
>>> lst = 4
>>> elems = fst, *other, lst
>>> elems
(1, 2, 3, 4)

Support de plusieurs unpacking dans les appels de fonctions

Cette généralisation se répercute sur les appels de fonctions ou méthodes : jusqu'à Python 3.4, un seul itérable pouvait être utilisé lors de l'appel à une fonction. Cette restriction est maintenant levée.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> def spam(a, b, c, d, e):
...    return a + b + c + d + e
...
>>> l1 = (2, 1)
>>> l2 = (4, 5)
>>> spam(*l1, 3, *l2)        # Légal en Python 3.5, impossible en Python 3.4
15                           # 2 + 1 + 3 + 4 + 5
>>> d1 = {"b": 2}
>>> d2 = {"d": 1, "a": 5, "e": 4}
>>> spam(**d1, c=3, **d2)    # Légal en Python 3.5, impossible en Python 3.4
15                           # 5 + 2 + 3 + 1 + 4
>>> spam(*(1, 2), **{"c": 3, "e": 4, "d": 5})
15                           # 1 + 2 + 3 + 5 + 4

Notez que si un nom de paramètre est présent dans plusieurs dictionnaires, c'est la valeur du dernier qui sera prise en compte.

D'autres généralisations sont mentionnées dans la PEP mais n'ont pas été implémentées à cause d'un manque de popularité parmi les developpeurs de Python. Il n'est cependant pas impossible que d'autres fonctionnalités soient introduites dans les prochaines versions.

Opérateur de multiplication matricielle – PEP 465

L'introduction d'un opérateur binaire n'est pas courant dans Python. Aucun dans la série 3.x, le dernier ajout semble être l'opérateur // dans Python 2.21. Regardons donc pour quelles raisons celui-ci a été introduit.

Signification

Ce nouvel opérateur est dédié à la multiplication matricielle. En effet, comme tous ceux ayant fait un peu de mathématiques algébriques le savent sûrement, on définit généralement la multiplication matricielle par l'opération suivante (pour des matrices $2*2$ ici) :

$$ \begin{pmatrix}a&b \\ c&d \end{pmatrix} \cdot \begin{pmatrix}e&f \\ g&h \end{pmatrix} = \begin{pmatrix}a*e+b*g&a*f+b*h \\ c*e+d*g&c*f+d*h \end{pmatrix} $$

Or il est souvent aussi nécessaire d'effectuer des multiplications terme à terme :

$$ \begin{pmatrix}a&b \\ c&d \end{pmatrix} * \begin{pmatrix}e&f \\ g&h \end{pmatrix} = \begin{pmatrix}a*e&b*f \\ c*e&d*h \end{pmatrix} $$

Tandis que certains langages spécialisés possèdent des opérateurs dédiés pour chacune de ces opérations2 il n'y a en Python rien de similaire. Avec la bibliothèque numpy, la plus populaire pour le calcul numérique dans l'éco-système Python, il est possible d'utiliser l'opérateur natif * pour effectuer une multiplication terme à terme, surcharge d'opérateur se rencontrant dans la plupart des bibliothèques faisant intervenir les matrices. Mais il n'existe ainsi plus de moyen simple pour effectuer une multiplication matricielle. Avec numpy, il est pour le moment nécessaire d'utiliser la méthode dot et d'écrire des lignes de la forme :

1
S = (A.dot(B) - C).T.dot(inv(A.dot(D).dot(A.T))).dot(A.dot(B) - C)

Celle-ci traduit cette formule :

$$ (A \times B - C)^T \times (A \times D \times A^T)^{-1} \times (A \times B - C) $$

Cela n'aide pas à la lecture… Le nouvel opérateur @ est introduit et dédié à la multiplication matricielle. Il permettra d'obtenir des expressions équivalentes de la forme :

1
S = (A @ B - C).T @ inv(A @ D @ A.T) @ (A @ B - C)

Un peu mieux, non ?

Impact sur vos codes

Cette introduction devrait être anodine pour beaucoup d'utilisateurs. En effet, aucun objet de la bibliothèque standard ne va l'utiliser3. Cet opérateur binaire servira principalement à des bibliothèques annexes, à commencer par numpy.

Si vous souhaitez supporter cet opérateur, trois méthodes spéciales peuvent être implémentées : __matmul__ et __rmatmul__ pour la forme a @ b et __imatmul__ pour la forme a @= b, de façon similaire aux autres opérateurs binaires.

À noter qu'il est déconseillé d'utiliser cet opérateur pour autre chose que les multiplications matricielles.

Motivation

L'introduction de cet opérateur est un modèle du genre : il peut sembler très spécifique mais est pleinement justifié. La lecture de la PEP, développée, est très instructive.

Pour la faire adopter, les principales bibliothèques scientifiques en Python ont préparé cette PEP ensemble pour arriver à une solution convenant à la grande partie de la communauté scientifique, assurant dès lors l'adoption rapide de cet opérateur. La PEP précise ainsi l’intérêt et les problèmes engendrés par les autres solutions utilisées jusque-là.

Enfin cette PEP a été fortement appuyée par la grande popularité de Python dans le monde scientifique. On apprend aussi que numpy est le module n’appartenant pas à la bibliothèque standard le plus utilisé parmi tous les codes Python présents sur Github et ce sans compter d'autres bibliothèques comme pylab ou scipy qui vont aussi profiter de cette modification et comptent parmi les bibliothèques les plus communes.

Support des coroutines – PEP 492

Introduction à la programmation asynchrone

Pour comprendre ce qu'est la programmation asynchrone, penchons-nous sur l'exemple de code suivant, chargé de télécharger le contenu de la page d’accueil de Zeste de Savoir et de l'enregistrer sur le disque par morceaux de 512 octets en utilisant la bibliothèque requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

def fetch_page(url, filename, chunk_size=512):

    # Nous effectuons une demande de connexion au serveur HTTP et attendons sa réponse
    response = requests.get(url, stream=True)

    # Nous vérifions qu'il n'y a pas eu d'erreur lors de la connexion
    assert response.status_code == 200

    # Nous ouvrons le fichier d'écriture
    with open(filename, mode='wb') as fd:

        # Nous récupérons le fichier de la page d'accueil par morceaux : à chaque
        # tour de boucle, nous demandons un paquet réseau au serveur et attendons
        # qu'il nous le donne
        for chunk in response.iter_content(chunk_size=chunk_size):

            # Nous écrivons dans le fichier de sortie, sur le disque
            fd.write(chunk)

if __name__ == "__main__":
    fetch_page('http://zestedesavoir.com/', 'out.html')

Lors d'un appel à ce genre de fonction très simple votre processeur passe son temps… à ne rien faire ! En effet, quand il n'attend pas le serveur Web, il patiente le temps que le disque effectue les opérations d'écriture demandées. Or tout cela est très lent à l'échelle d'un processeur, et tous ces appels à des services extérieurs sont très courants en Web : connexion et requêtes à une base de données, appels à une API externe, etc. La perte de temps est de ce fait considérable.

La programmation dite asynchrone cherche à résoudre ce problème en permettant au développeur d'indiquer les points où la fonction attend un service extérieur (base de données, serveur Web, disque dur, etc.), appelé entrées/sorties (en anglais : io). Le programme principal peut ainsi faire autre chose pendant ce temps, comme traiter la requête d'un autre client. Pour cela une boucle évenementielle est utilisée. Le coeur de l'application est alors une fonction qu'on pourrait résumer par les opérations suivantes :

  1. Prendre une tâche disponible
  2. Exécuter la tâche jusqu'à ce qu'elle soit terminée ou qu'elle doive attendre des entrées/sorties
  3. Dans le second cas, la mettre dans une liste de tâches en attente
  4. Mettre dans la liste des tâches disponibles les tâches en attente ayant reçu leurs données
  5. Retourner en 1

Prenons un exemple, avec un serveur Web :

  • Un client A demande une page, appelant ainsi une fonction f
  • Un client B demande une page, appelant une autre fonction g, appel mis en attente vu que le processeur est occupé avec le client A
  • L'appel à f atteint une écriture dans un fichier, et se met en pause le temps que le disque fasse son travail
  • g est démarrée
  • L'écriture sur le disque de f est terminée
  • g atteint une demande de connexion à la base de données et se met en pause le temps que la base réagisse
  • L'appel à f est poursuivi et terminé
  • Un client C demande une page
  • etc.

En découpant une fonction par morceaux, la boucle peut exécuter du code pendant que les longues opérations d'entrées/sorties d'un autre morceau de code se déroulent à l'extérieur…

Nous nous comportons naturellement de cette manière dans la vie. Par exemple, lorsque vous effectuez un rapport que vous devez faire relire à votre chef : après lui avoir fait parvenir une première version, vous allez devoir attendre qu'il l'ait étudié avant de le corriger ; plutôt que de patienter bêtement devant son bureau, vous vaquez à vos occupations, et serez informé lorsque le rapport pourra être repris.

La programmation asynchrone en Python

La programmation asynchrone a été remise récemment au goût du jour par Node.js. Mais Python n'est pas en reste, avec de multiples bibliothèques pour gérer ce paradigme : Twisted existe depuis 13 ans (2002), Tornado a été libérée par FriendFeed il y a 6 ans (2009), à l'époque où le développement de gevent commençait.

Au moment de la version 3.4 de Python, Guido van Rossum, créateur et BDFL du langage, a décidé de standardiser cette approche en synthétisant les idées des bibliothèques populaires précédemment citées et d'intégrer une implémentation typique dans la bibliothèque standard : c'est le module asyncio. Ce dernier n'a pas pour but de remplacer les bibliothèques mentionnées, mais de proposer une implémentation standardisée d'une boucle événementielle et de mettre à disposition quelques fonctionnalités bas niveau (Tornado peut ainsi être utilisé avec asyncio).

Tandis que Node.js, par exemple, emploie ce que l'on appelle des fonctions de rappel (callbacks) pour ordonnancer les différentes étapes d'un algorithme, asyncio utilise des coroutines, lesquelles permettent d'écrire des fonctions asynchrones avec un style procédural. Les coroutines ressemblent beaucoup aux fonctions, à la différence près que leur exécution peut être suspendue et reprise à plusieurs endroits dans la fonction. Python dispose déjà de constructions de ce genre : les générateurs4. C'est ainsi avec les générateurs que asyncio a été initialement développé.

Pour illustrer cela, reprenons l'exemple décrit plus haut, avec cette fois Python 3.4, asyncio, la bibliothèque aiohttp5 pour faire des requêtes HTTP et la bibliothèque aiofiles pour écrire dans des fichiers locaux, le tout de façon asynchrone :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import asyncio
import aiohttp
import aiofiles

@asyncio.coroutine           # On déclare cette fonction comme étant une coroutine
def fetch_page(url, filename, chunk_size=512):
    # À la ligne suivante, la fonction est interrompue tant que la requête n'est pas revenue
    # Le programme peut donc vaquer à d'autres tâches
    response = yield from aiohttp.request('GET', url)

    # Le serveur HTTP nous a répondu, on reprend l'appel de la fonction à ce
    # niveau et vérifie que la connexion est correctement établie
    assert response.status == 200

    # De même, on suspend ici la fonction le temps que le fichier soit créé
    fd = yield from aiofiles.open(filename, mode='wb')
    try:
        while True:
            # On demande un paquet au serveur Web et interrompt la coroutine le
            # temps qu'il nous réponde
            chunk = yield from response.content.read(chunk_size)

            if not chunk:
                break

            # On suspend la coroutine le temps que le disque dur écrive dans le fichier
            yield from fd.write(chunk)

    finally:
        # On ferme le fichier de manière asynchrone
        yield from fd.close()

if __name__ == "__main__":
    # On démarre une tâche dans la boucle évènementielle.
    # Vu qu'il n'y en a qu'une seule, procéder de manière asynchrone n'est pas très utile ici.
    asyncio.get_event_loop().run_until_complete(fetch_page('http://zestedesavoir.com/', 'out.html'))

Cet exemple nous montre comment utiliser les générateurs et asyncio pour obtenir un code asynchrone lisible. Toutefois :

  • Il y a détournement du rôle d'origine de l'instruction yield from. Sans le décorateur la fonction pourrait être facilement confondue avec un générateur classique, sans rapport avec de l'asynchrone.
  • Tandis que l'exemple d'origine utilisait with pour assurer la fermeture du fichier même en cas d'exception, nous sommes obligés ici de reproduire manuellement le comportement de cette instruction. En effet with n'est pas prévue pour appeler des coroutines, donc on ne peut effectuer de manière asynchrone les opérations d'entrée (ligne 16) ni de sortie (ligne 31).
  • De la même façon, l'instruction for ne permet pas de lire un itérable de manière asynchrone, comme fait ligne 21. Il nous faut donc passer peu élégamment par une boucle while.

Les nouveaux mot-clés

Python 3.5 introduit deux nouveaux mot-clés pour résoudre les problèmes précédemment cités : async et await. De l'extérieur, async vient remplacer le décorateur asyncio.coroutine et await l'expression yield from.

Mais les modifications sont plus importantes que ces simples synonymes. Tout d'abord, une coroutine déclarée avec async n'est pas un générateur. Même si en interne les deux types de fonctions partagent une grande partie de leur implémentation, il s'agit de constructions du langage différentes et il est possible que les différences se creusent dans les prochaines versions de Python. Pour marquer cette distinction et le fait que des générateurs sont une forme restreinte de coroutines, il est possible dans Python 3.5 d'utiliser des générateurs partout où une coroutine est attendue, mais pas l'inverse. Le module asyncio continue ainsi à supporter les deux formes.

L'ajout de ces mot-clés a aussi été l'occasion d'introduire la possibilité d'itérer de manière asynchrone sur des objets. Ce support, assuré en interne par les nouvelles méthodes __aiter__ et __anext__ pourra être utilisé par des bibliothèques comme aiohttp pour simplifier les itérations grâce à la nouvelle instruction async for.

De la même façon, des context managers asynchrones font leur apparition via l'instruction async with, en utilisant les méthodes __aenter__ et __aexit__.

Il est donc possible, et conseillé, en Python 3.5 de ré-écrire le code précédent de la manière suivante.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import asyncio
import aiohttp
import aiofiles

# On déclare cette fonction comme étant une coroutine
async def fetch_page(url, filename, chunk_size=512):
    # À la ligne suivante, la fonction est interrompue tant que la connexion n'est pas établie
    response = await aiohttp.request('GET', url)
    assert response.status == 200

    # Ouverture, puis fermeture, du fichier de sortie de manière asynchrone
    async with aiofiles.open(filename, mode='wb') as fd:

        # On lit le contenu au fur et à mesure que les paquets arrivent et on interrompt l'appel en attendant
        async for chunk in response.content.read_chunk(chunk_size):

            await fd.write(chunk)

if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(fetch_page('http://zestedesavoir.com/', 'out.html'))

L'exemple ci-dessus montre clairement l'objectif de l'ajout de async for et async with : si les async et await sont ignorés, la coroutine est fortement similaire à une implémentation utilisant des fonctions classiques présentées en début de section.

Le dernier exemple de codes est un aperçu de ce que pourrait être la programmation asynchrone avec Python 3.5. À l'heure où ces lignes sont écrites, aiohttp et aiohttp ne supportent pas encore les nouvelles instructions async for et async with. De la même manière, pour l'instant, seul asyncio gère les mot-clés async et await au niveau de sa boucle événementielle.

Enfin, notez que les expressions await, au-delà de leur précédence beaucoup plus faible, sont moins restreintes que les yield from et peuvent être placées partout où une expression est attendue. Ainsi les codes suivants sont valides.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if await foo:
    pass

# /!\ Différent de `async with`
# Ici, c'est `bar` qui est appelé de manière asynchrone et non `bar.__enter__`
with await bar:
     pass

while await spam:
     pass

await spam + await egg

Impact sur vos codes

Concernant vos codes existants, cette version introduit logiquement la dépréciation de l'utilisation de async et await comme noms de variable. Pour le moment, un simple avertissement sera émis, lequel sera transformé en erreur dans Python 3.7. Ces modifications restent donc parfaitement rétro-compatibles avec Python 3.4.

Si vous utilisez asyncio, rien ne change pour vous et vous pouvez continuer à employer les générateurs si vous souhaitez conserver le support de Python 3.4. L'ajout des méthodes de la forme __a****__ n'est donc pas obligatoire, mais peut vous permettre cependant de supporter les nouveautés de Python 3.5.

Annotations de types – PEP 484

Les annotations de fonctions existent depuis Python 3 et permettent d'attacher des objets Python aux arguments et à la valeur de retour d'une fonction ou d'une méthode :

1
2
def ma_fonction(param: str, param2: 42) -> ["Mon", "annotation"]:
    pass

Depuis le début, il est possible d'utiliser n'importe quel objet valide comme annotation. Ce qui peut surprendre, c'est que l'interpréteur n'en fait pas d'utilisation particulière : il se contente de les stocker dans l'attribut __annotations__ de la fonction correspondante :

1
2
>>> ma_fonction.__annotations__
{'param2': 42, 'return': ['Mon', 'annotation'], 'param': <class 'str'>}

La PEP 484 introduite avec Python 3.5 fixe des conventions pour ces annotations : elles deviennent réservées à « l'allusion de types » (Type Hinting), qui consiste à indiquer le type des arguments et de la valeur de retour des fonctions :

1
2
3
# Cette fonction prend en argument une chaîne de caractères et en retourne une autre.
def bonjour(nom: str) -> str:
    return 'Zestueusement ' + nom

Toutefois, il ne s'agit encore que de conventions : l'interpréteur Python ne fera rien d'autre que de les stocker dans l'attribut __annotations__. Il ne sera même pas gêné par une annotation d'une autre forme qu'un type.

Pour des indications plus complètes sur les types, un module typing est introduit. Il permet de définir des types génériques, des tableaux, etc. Il serait trop long de détailler ici toutes les possibilités de ce module, donc nous nous contentons de l'exemple suivant.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# On importe depuis le module typing :
# - TypeVar pour définir un ensemble de types possibles
# - Iterable pour décrire un élément itérable
# - Tuple pour décrire un tuple
from typing import TypeVar, Iterable, Tuple

# Nous définissons ici un type générique pouvant être un des types numériques cités.
T = TypeVar('T', int, float, complex)
# Vecteur représente un itérateur (list, tuple, etc.) contenant des tuples 
# comportant chacun deux éléments de type T.
Vecteur = Iterable[Tuple[T, T]]

# Pour déclarer un itérable de tuples, sans spécifier le contenu de ces derniers, 
# nous pouvons utiliser le type natif :
# Vecteur2 = Iterable[tuple]
# Les éléments présents dans le module typing sont là pour permettre une 
# description plus complète des types de base.

# Nous définissons une fonction prenant un vecteur en argument et renvoyant un nombre
def inproduct(v: Vecteur) -> T:
    return sum(x*y for x, y in v)

vec = [(1, 2), (3, 4), (5, 6)]
res = inproduct(vec)
# res == 1 * 2 + 3 * 4 + 5 * 6 == 44

Néanmoins, les annotations peuvent surcharger les déclarations et les rendre peu lisibles. Cette PEP a donc introduit une convention supplémentaire : les fichiers Stub. Il a été convenu que de tels fichiers, facultatifs, contiendraient les annotations de types, permettant ainsi de profiter de ces informations sans polluer le code source. Par exemple, le module datetime de la bibliothèque standard comporterait un fichier Stub de la forme suivante.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class date(object):
    def __init__(self, year: int, month: int, day: int): ...
    @classmethod
    def fromtimestamp(cls, timestamp: int or float) -> date: ...
    @classmethod
    def fromordinal(cls, ordinal: int) -> date: ...
    @classmethod
    def today(self) -> date: ...
    def ctime(self) -> str: ...
    def weekday(self) -> int: ...

Ces fichiers permettent aussi d'annoter les fonctions de bibliothèques écrites en C ou de fournir des annotations de types aux modules utilisant déjà les annotations pour autre chose que le type hinting.

Tout comme rien ne vous oblige à utiliser les annotations pour le typage, rien ne vous force à vous servir du module typing ou des fichiers Stub : l'interpréteur n'utilisera ni ne vérifiera toujours pas ces annotations ; le contenu des fichiers Stub ne sera même pas intégré à l'attribut __annotations__. Ne vous attendez donc pas à une augmentation de performances en utilisant les annotations de types : l'objectif de cette PEP est de normaliser ces informations pour permettre à des outils externes de les utiliser. Les premiers bénéficiaires seront donc les EDI, les générateurs de documentation (comme Sphinx) ou encore les analyseurs de code statiques comme mypy, lequel a fortement inspiré cette PEP et sera probablement le premier outil à proposer un support complet de cette fonctionnalité.

Les annotations de types ne sont donc qu'un ensemble de conventions, comme il en existe déjà plusieurs dans le monde Python (PEP 333, PEP 8, PEP 257, etc.). Cet ajout n'a donc aucun impact direct sur vos codes mais permettra aux outils externes de vous fournir du support supplémentaire.


  1. Ces ajouts sont tellement peu courants qu'il est difficile de trouver des traces de ces modifications. L'opérateur // semble être le seul ajout de toute la série 2. 

  2. Par exemple, avec Matlab et Julia, * / .* servent respectivement pour la multiplication matricielle et terme à terme. 

  3. Ce qui est rare mais existe déjà. En particulier l'objet Elipsis créé avec ...

  4. Les générateurs sont en théorie des formes particulières de coroutines, mais leur implémentation en Python leur confère pleinement le statut de coroutine. 

  5. Bibliothèque qui propose des fonctions d'entrées/sorties sur le protocole HTTP. 

De plus petits changements

  • PEP 441 : ajout d'un module zipapp, pour améliorer le support et la création d'applications packagées sous forme d'archive zip, introduits dans Python 2.6.
  • PEP 461 : retour de l'opérateur de formatage % pour les types de données byte et bytearray, de la même façon qu'il était utilisable pour les types chaînes (str) dans Python 2.7.
  • PEP 488 : la suppression des fichiers pyo.
  • PEP 489 : un changement de la procédure d'import des modules externes.
  • PEP 471 : une nouvelle fonction os.scandir(), pour itérer plus efficacement sur le contenu d'un dossier sur le disque qu'en utilisant os.walk(). Cette dernière fonction l'utilise maintenant en interne et est de trois à cinq fois plus rapide sur les systèmes POSIX et de sept à vingt fois plus rapide sur Windows.
  • PEP 475 : l'interpréteur va maintenant automatiquement retenter le dernier appel système lorsqu'un signal EINTR est reçu, évitant aux codes utilisateurs de s'en occuper.
  • PEP 479 : le comportement des générateurs changent lorsqu'une exception StopIteration est déclenchée à l'intérieur. Avec cette PEP, cette exception est transformée en RuntimeError plutôt que de quitter silencieusement le générateur. Cette modification cherche à faciliter le déboguage et clarifie la façon de quitter un générateur : utiliser return plutôt que de générer une exception. Cette PEP n'étant pas rétro-compatible, vous devez manuellement l'activer avec from __future__ import generator_stop pour en profiter.
  • PEP 485 : une fonction isclose est ajoutée pour tester la « proximité » de deux nombres flottants.
  • PEP 486 : le support de virtualenv dans l'installateur de Python sous Windows est amélioré et simplifié.
  • Ticket 9951 : pour les types byte et bytearray, une nouvelle méthode hex() permet maintenant de récupérer une chaîne représentant le contenu sous forme hexadécimale.
  • Ticket 16991 : l'implémentation en C, plutôt qu'en Python, des OrderedDict (dictionnaires ordonnés) apporte des gains de quatre à cent sur les performances.

De plus, comme d’habitude, tout un tas de fonctions, de petites modifications et de corrections de bugs ont été apportées à la bibliothèque standard, qu'il serait trop long de citer entièrement.

Notons aussi que le support de Windows XP est supprimé. Cela ne veut pas dire que Python 3.5 ne fonctionnera pas dessus, mais rien ne sera fait par les développeurs pour cela.

Enfin, le développement de CPython 3.5 (à partir de la première release candidate) s'est effectué sur BitBucket plutôt que sur le traditionnel dépôt auto-hébergé de la fondation, afin de bénéficier des fonctionnalités proposées par le site ; le gestionnaire de versions est toujours Mercurial. Il n'est pas encore connu si les prochaines versions suivront ce changement et si le développement de CPython se déplacera sur BitBucket : il s'agit d'une expérimentation que les développeurs vont évaluer. Pour le moment, les branches pour les versions 3.5.1 et 3.6 restent sur https://hg.python.org/cpython au moins jusqu'à la sortie de Python 3.5.

Ce que l'on peut attendre pour la version 3.6

Il est impossible de savoir précisément ce qui sera disponible dans la prochaine version du langage et de l'interpréteur. En effet aucune entreprise ou groupe ne décide par avance des fonctionnalités. Les changements inclus dépendent des propositions faites, majoritairement, sur deux mailling list :

  • python-dev quand cela concerne des changements internes à l'intepréteur CPython.
  • python-ideas quand cela concerne des idées générales à propos du langage.

S'ensuit une série de débats qui, si les idées sont acceptées par une majorité de developpeurs principaux, conduit à la rédaction d'une PEP, qui sera elle-même acceptée ou refusée (par Guido, le BDFL, ou par un autre développeur si Guido est l'auteur de la PEP). En l'absence de feuilles de route définies à l'avance, les PEP peuvent arriver tard dans le cycle de développement. Ainsi, la PEP 492 sur async et await n'a été créé qu'un mois avant la sortie de la première bêta de Python 3.5.

Malgré ces incertitudes, on peut deviner quelques modifications probables. Attention, cette section reste très spéculative…

Continuité des changements introduits dans Python 3.5

Trois thématiques de modifications amorcées dans Python 3.4 et surtout Python 3.5 pourraient être de nouveau sources d'ajouts importants dans Python 3.6 :

  • La programmation asynchrone avec asyncio et les coroutines
  • Les indications de types
  • La généralisation de l'unpacking

Les deux premiers éléments sont officiellement en attente de retours de la part des utilisateurs de Python. Les développeurs de l'interpréteur attendent de connaître les problèmes et limitations rencontrées en utilisant ces fonctionnalités pour les peaufiner dans les prochaines versions. Python 3.6 devrait donc logiquement voir des améliorations dans ces deux sections en profitant des retours. Déjà quelques remarques ont été faites, comme la complexité de mêler des fonctions synchrones et asynchrones.

La généralisation de l'unpacking pourrait elle aussi continuer. La PEP 448 proposait initialement d'autres généralisations qui n'ont pas été retenues pour Python 3.5 par manque de temps. Elles seront donc propablement rapidement rediscutées. De plus, ces ajouts dans Python 3.5 ont donné des idées à d'autres développeurs et quelques nouvelles modifications ont été déjà proposées.

Conservation de l'ordre des arguments fournis à **kwargs lors de l'appel aux fonctions

Cette PEP a déjà été acceptée mais n'a pas pu être implémentée dans la version 3.5. Il est donc possible qu'elle soit finalement présente dans la prochaine version. L'idée est que les fonctions puissent connaître l'ordre dans lequel les arguments nommés ont été passés. Prenons un exemple :

1
2
3
4
5
def spam(**kwargs):
    for k, v in kwargs.items():
        print(k, v)

spam(a=1, b=2)

Avec Python 3.5, comme toutes versions de CPython et presque toutes les autres implémentations (sauf PyPy), nous ne pouvons pas savoir si le résultat sera :

Nom Valeur
a 1
b 2

ou

Nom Valeur
a 2
b 1

En effet, un dictionnaire est utilisé pour passer les arguments. Or ceux-ci ne garantissent pas d'ordre sur leurs clés. Pourtant, Python dispose d'une classe OrderedDict depuis Python 3.1. L'implémentation de cette PEP se résumerait donc à remplacer l'objet utilisé en interne, un dictionnaire basique, par un dictionnaire ordonné. Cependant, jusqu'à maintenant, cet objet était défini en Python. L'implémentation était donc loin d'être la plus efficace possible et ne pouvait pas être utilisée pour un élément aussi critique du langage. C'est pour cette raison que la classe a été réimplémentée en C pour Python 3.5, comme noté dans la section « De plus petits changements ». Maintenant, plus rien ne semble bloquer l'implémentation de cette PEP, qui devrait donc voir le jour dans Python 3.6.

Le principal intérêt de cette modification est que le fonctionnement actuel est contre-intuitif pour bon nombre d'utilisateurs connaissant mal le fonctionnement interne de Python. D'autres raisons sont invoquées dans la PEP :

  • L'initialisation d'un OrderedDict est un cas d'appel où l'ordre des paramètres importe, et de tels objets pourraient maintenant être créés en utilisant des arguments nommés, comme le permettent les dictionnaires.
  • La sérialisation : dans certains formats l'ordre d'apparition des données a de l'importance (ex : l'ordre des colonnes dans un fichier CSV). Cette nouvelle possibilité permettrait de les définir plus facilement, en même temps que des valeurs par défaut. Elle permettrait aussi à des formats comme XML, JSON ou Yaml de garantir l'ordre d'apparition des attributs ou clés qui sont enregistrés dans les fichiers.
  • Le débogage : le fonctionnement actuel pouvant être aléatoire, il peut être compliqué de reproduire certains bugs. Si un bug apparait selon l'ordre de définition des arguments, le nouveau comportement facilitera leur correction.
  • La priorité à donner aux arguments pourrait être spécifiée selon l'ordre de leur déclaration.
  • Les namedtuple pourraient être définis aisément avec une valeur par défaut.

Et probablement d'autres utilisations qui n'ont pas encore été envisagées.

Proprités de classes

Toujours dans les petites modifications, un nouveau décorateur disponible de base devrait être introduit : @classproperty. Si vous connaissez le modèle objet de Python, son nom devrait vous suffir pour deviner son but : permettre de définir des propriétés au niveau de classes. Par exemple si vous souhaitez conserver au niveau de la classe le nombre d'instances créées et des statistiques sur vos appels :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Spam:
    _n_instance_created = 0
    _n_egg_call = 0

    def __init__(self):
        self.__class__._n_instance_created += 1

    def egg(self):
        print("egg")
        self.__class__._n_egg_call += 1

    @classproperty
    def mean_egg_call(cls):
        return (cls._n_egg_call / cls._n_instance_created) if cls._n_instance_created > 0 else 0

spam = Spam()

spam.egg()
spam.egg()
spam.egg()

print(spam.mean_egg_call)   # => 3

spam2 = Spam()

# Les 3 expressions suivantes sont équivalentes
print(spam.mean_egg_call)    # => 1.5
print(spam2.mean_egg_call)   # => 1.5
print(Spam.mean_egg_call)    # => 1.5  , propriété au niveau de la classe !

Cet ajout a été proposé sur la mailling-list python-ideas. La mise en place d'une implémentation propre et complète de ce comportement est compliquée sans définir une méta-classe. Une solution générique implique donc de le rajouter dans le modèle objet de Python. Guido a rapidement donné son aprobation et un ticket a été créé en attendant qu'un des développeurs de CPython l'implémente dans le coeur de l'interpréteur. Cela pourrait donc être l'une des premières nouvelles fonctionnalités confirmées pour la version 3.6.

Sous-interpréteurs

Cette section peut nécessiter des notions avancées de programmation système pour être comprise, en particulier la différence entre un thread et un processus, ainsi que leur gestion dans Python.

L'écriture de code concurrent en Python est toujours un sujet chaud. À ce jour, trois solutions existent dans la bibliotèque standard :

  • asyncio, bien que limitée à un seul thread, permet d'écrire des coroutines s'exécutant en concurrence en tirant partie des temps d'attente introduits par les entrées/sorties.
  • threading permet de facilement exécuter du code sur plusieurs threads, mais leur exécution reste limitée à un seul coeur de processeur à cause du GIL.
  • multiprocessing, en forkant l'interpréteur, permet d'éxecuter plusieurs codes Python en parallèle sans limitation et exploitant pleinnement les ressources calculatoires des processeurs.

Le GIL est une construction implémentée dans de nompreux interpéteurs (CPython, Pypy, Ruby, etc.). Ce mécanisme bloque l'interpréteur pour qu'à chaque instant, un seul code puisse être exécuté. Ce sytème permet de s'assurer que du code éxécuté sur plusieurs threads ne va pas poser de problèmes de concurrence sur la mémoire, sans vraiment ralentir les codes n'utilisant qu'un seul thread. Malheureusement cela nous empèche d'exploiter les architectures multi-coeurs de nos processeurs.

La multiplicité des solutions ne résoud pas tout. En effet les limitation des threads en Python les rendent inutiles quand le traitement exploite principalement le processeur. L'utilisation de multiprocessing est alors possible, mais a un coût :

  • Le lancement d'un processus entraine un fork au niveau du système d'exploitation, ce qui prend plus de temps et de mémoire que le lancement d'un thread (presque gratuit en comparaison).
  • La communication entre les processus est aussi plus longue. Tandis que les threads permettent d'exploiter un espace partagé en mémoire, rendant leurs communications directes, les processus nécessitent de mettre en place des mécanismes complexes, appelés IPC.

Une proposition sur python-ideas a été formulée au début de l'été pour offrir une nouvelle solution intermédiare entre les threads et les processus : des sous-interpréteurs (subinterpreters), en espérant que cela puisse définitivement contenter les développeurs friands de codes concurrents.

À partir d'un nouveau module subinterpreters, reprenant l'interface exposée par les modules threading et multiprocessing, CPython permettrait de lancer dans des threads différents interpréteurs, chacun chargé d'executer un code Python. Le fait que ce soient des threads rendrait ce module beaucoup plus léger à utiliser que multiprocessing. L'interpréteur se chargerait de lancer ces threads et partagerait le maximum de son implémentation entre eux. Le plus intéressant est que ce mécanisme permettrait d'éluder le GIL. En effet, chaque sous-interpréteur disposerait d'un espace de noms propre et indépendant. Chacun aurait donc un GIL, mais ceux-ci seraient indépendants. D'un point de l'ensemble, la majorité des opérations transformeraient le GIL en LIL : Local interpreter lock. Chaque sous-interpréteur pourrait ainsi fonctionner sur un coeur du processus séparé sans aucun problème, permettant ainsi au développeur de tirer pleinement partie des processeurs modernes. Enfin, il faut savoir que CPython possède déjà en interne la notion de sous-interpréteurs, le travail à réaliser consisterait donc à exposer ce code C pour le rendre disponible en Python.

Évidement, cette proposition n'est pas une solution miracle. Tout n'est pas si simple et beaucoup d'élements doivent encore être discutés. En particulier les moyens de communication entre les sous-interpréteurs (probablement via des queues, comme pour multiprocessing) et tout un tas de petits détails d'implémentation. Mais la proposition a reçu un accueil très positif et l'auteur est actuellement en train de préparer une PEP à ce sujet.

Interpolation de chaines

Les interpolations directes de chaînes de caractères existent dans de nombreux langages : PHP, C#, Ruby, Swift, Perl, etc. En voici un exemple en Perl :

1
2
3
4
5
my $a = 1;
my $b = 2;

print "Resultat = a + b = $a + $b = @{[$a+$b]}\n";
# Imprime "Resultat = a + b = 1 + 2 = 3"

Il n'existe pas d'équivalent direct en Python ; le plus proche pourrait ressembler à l'exemple suivant :

1
2
3
4
5
a, b = 1, 2

print("Resultat = a + b = %d + %d = %d" % (a, b, a + b))
# ou
print("Resultat = a + b = {a} + {b} = {c}".format(a=a, b=b, c=a + b))

Nous voyons ainsi qu'il est nécessaire de passer explicitement les variables et, même s'il est possible d'utiliser locals() ou globals() pour s'en passer, il n'est possible d'évaluer une expression, comme ici l'addition, qu'à l'extérieur de la chaîne de caractères puis d'injecter le résultat dans cette dernière.

La première proposition effectuée, formalisée par la PEP 498, propose de rajouter cette possibilité dans Python grâce à un nouveau préfixe de chaîne, f, pour format-string, dont voici un exemple d'utilisation, issu de la PEP :

1
2
3
4
5
6
7
8
>>> import datetime
>>> name = 'Fred'
>>> age = 50
>>> anniversary = datetime.date(1991, 10, 12)
>>> f'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.'
'My name is Fred, my age next year is 51, my anniversary is Saturday, October 12, 1991.'
>>> f'He said his name is {name!r}.'
"He said his name is 'Fred'."

Cette proposition a fait des émules. Plusieurs développeurs ont mit en évidence que la méthode d'interpolation peut dépendre du contexte. Par exemple un module de base de données comme sqlite3 propose un système de substitution personnalisé pour éviter les attaques par injection ; les moteurs de templates, eux, pour produire du HTML, vont aussi faire des substitutions pour échapper certains caractères, comme remplacer < ou > par, respectivement, &lt; ou &gt;. La PEP 501 propose donc aux développeurs de définir leurs propres méthodes d'interpolation, adaptées au contexte.

Guido a déjà accepté la première proposition afin qu'elle soit implémentée dans Python 3.6. La deuxième est plus générale mais plus complexe, et peut ainsi poser plusieurs problèmes. Aucune décision n'a encore été prise et les débats à ce sujet ont encore lieu sur la mailling list concernant cette possibilité et la forme qu'elle pourrait prendre. Le patch pour la PEP 498 accepté est presque prêt et sera donc vraisemblablement la première nouvelle fonctionnalité de Python 3.6 ; l'implémentation de la PEP 501 dépendra de la suite des discussions.


Vous en conviendrez, cette version est sans doute l'une des plus importantes en termes de nouvelles fonctionnalités. Qui plus est, aujourd'hui, plus aucune fonctionnalité n'est introduite dans Python 2.7 et la plupart des bibliothèques populaires sont compatibles avec Python 3, nous pouvons espérer que cette version permette enfin de parachever le basculement de Python 2 à Python 3.

Vous pouvez dès maintenant télécharger cette nouvelle version sur le site officiel de Python.

Enfin, si certains points de cet article ne sont pas clairs pour vous, n'hésitez pas à nous en faire part en commentaire et nous nous efforcerons de clarifier vos interrogations !

32 commentaires

C'est fort de voir autant de chose nouvelles en seulement 1 an et demi !

Et bravo pour l'article !

Dans tout la partie sur l'asynchone, les exemples viennent beaucoup de problèmes liés au réseaux. J'imagine que c'est le champ d'application principal, mais existe-t-il d'autres cas dans lesquels ça peut être utile ?

+1 -0

C'est fort de voir autant de chose nouvelles en seulement 1 an et demi !

Et bravo pour l'article !

Dans tout la partie sur l'asynchone, les exemples viennent beaucoup de problèmes liés au réseaux. J'imagine que c'est le champ d'application principal, mais existe-t-il d'autres cas dans lesquels ça peut être utile ?

Gabbro

J'ai un (ou deux) articles de vulgarisation sur le sujet en préparation.

En fait les réseaux sont le champ d'application le plus évident parce que les serveurs connectés à tout un tas d'autres services (Web, Cloud…) sont les applis qui font instinctivement le plus d'IO. De plus les mécanismes bas niveau sur lesquels reposent les entrées/sorties asynchrones (select, poll, etc.) fonctionnent très bien avec les sockets sous tous les OS, mais Windows, pour ne pas le citer, est l'un des seuls à ne pas exposer la même abstraction (les file descriptors) pour les sockets et les autres flux de données (notamment les fichiers), du coup il est plus difficile sous cet OS d'obtenir la même fonctionnalité que sous les Unixoides.

Par contre, on peut imaginer programmer beaucoup de choses de façon asynchrone. Notamment la programmation de jeux vidéo (2D) repose pas mal sur des boucles événementielles, donc ça m'étonnerait que ce paradigme ne trouve pas preneur dans d'autres domaines que les applis réseaux (c'est-à-dire qu'il passe aussi côté client).

Intuitivement, je le verrais bien se populariser également dans la programmation système. Puisque les fichiers, les pipes et les sockets Unix sont supportés nativement par asyncio (sans le configurer pour qu'il exécute une boucle événementielle autre que celle par défaut).

+4 -0

L'asynchrone avec une boucle événementielle tel que asyncio ou node ne sont vraiment utile que quand le processeur doit se retrouver à attendre. Du coup il n'y a presque que quand tu as des entrees/sorties que c'est vraiment utile. Les cas typiques sont les requêtes web, l'appel à une base de données, l'accès à des fichiers au disque.

Avec asyncio il est assez facile de mapper un processus ou un thread pour exécuter ça en dehors du thread principale. Tu peux donc imaginer lancer des calculs sur un processus externe et le récupérer pour contourner le GIL. Mais c'est aussi possible sans asyncio.

Plus que le réseau ce sont les entrées sorties qui ralentissent et sont contournées avec ce genre de système. Le réseau étant un des access les plus lent, ce sont donc surtout les appli qui l'utilise massivement qui voient un gain à utiliser l'asynchrone.

Si on va par là, la programmation asynchrone n'a pas besoin d'asyncio dans l'absolu.

Par contre le fait que les coroutines deviennent des constructions explicites du langage va aider à rendre le code asynchrone beaucoup plus intuitif à lire. Et c'est un choix d'autant plus intelligent pour Python, puisque cette techno est très utilisée pour composer des applis entre elles et communiquer avec.

Il suffit de jeter un oeil aux libs accessibles sur asyncio.org pour se rendre compte qu'asyncio est un choix particulièrement judicieux pour faire communiquer un serveur avec des bases de données hétéroclites (Redis, couchdb, memcache) ou des services web (aiohttp, …). Et vu que tout ça se passe dans un interpréteur Python, la limitation à un coeur de CPU devient un argument de vente : "Tu veux paralléliser ? Aucun problème, lance une ou deux instances de plus, ton code est déjà scalable".

Edit : d'où le fait que les subinterpreters aient reçu un si bon accueil, d'ailleurs. On peut imaginer lancer plusieurs instances du serveur asynchrone, chacun dans un subinterpreter, GIL-free, et se servir de l'interpréteur principal pour dispatcher les requêtes entre les instances, pour équilibrer la charge… :)

+0 -0

Merci pour cet article. J'étais un peu douteux à propos plusieurs des ajouts avant de lire l'article, mais celui-ci a bien expliqué les motivations des nouvelles fonctionalités et je comprends mieux pourquoi elles seront les bienvenues. :)

Dans l'exemple de la section sur la conservation de l'ordre des argument fournis à **kwargs, je ne sais pas si c'est une erreur ou si je n'ai pas bien compris, mais est-ce que les arguments a et b peuvent vraiment se retrouver avec des valeurs différentes? Je conçois que l'ordre dans lequel ils sont reçus n'est pas défini, mais qu'ils puissent changer de valeur par rapport à ce qui est passé me semble particulièrement étrange.

Pour ce qui est des constructions async for et async with, est-ce que je me plante en les considérant "comme si" c'étaient des for et with normaux mais avec await avant les appels à __iter__, __next__, __enter__ et __exit__? Je me doute bien que ce n'est pas implémenté comme ça, mais j'essais de m'imaginer un modèle mental simple pour réfléchir à ces nouvelles constructions.

Enfin, ça n'est pas spécifique à la version 3.5, mais je n'avais jamais vraiment vu d'exemples de asyncio. Ce n'est pas un peu redondant d'avoir des modules spécifiquement pour les opérations asynchrones (aiofiles, aiohttp)? Ne serait-il pas plus élégant de pouvoir écrire directement async with open(...) plutôt que d'aller chercher une version alternative de open?

Merci d'avance à ceux qui pourront m'éclairer sur ces quelques points. :)

Alors :

  • pour les ordres de kwargs, en effet il y a une erreur. J'aurai du lieux relire. Evidement les valeurs de à et b seront toujours les mêmes, seul l'ordre d'impression sera différents… Bref c'est un peu n'importe quoi ce passage. Je corigerais quand je pourrais.
  • pour async with et async for, oui c'est exactement ça.
  • asyncio est en gros l'implémentation de référence d'une boucle événementielle. Si il faut passer par des libs c'est qu'il y a plusieurs raisons. La première et principale est que ce serait trop gros. Ce qui est maintenant aiohttp était à l'origine inclus dans asyncio mais ça commençait à devenir trop gros, ils ont préféré splitter. asyncio se concentre à fournir les bases bas niveau, à charge de libs externes de fournir les modules spécifique.

Notons qu'en théorie open pourrait fournit de quoi faire un async with puisque les méthodes spéciales sont differentes. Pour l'instant ce n'est pas le cas. Ça viendra peut être, peut être pas. En réalité il est assez dur de faire un module d'Io sur les fichiers asynchrone et multiplateforme. Asyncio à encore un status de "non finalisé" officiellement. Il est possible que ça vienne un jour.

Pour ce qui est des constructions async for et async with, est-ce que je me plante en les considérant "comme si" c'étaient des for et with normaux mais avec await avant les appels à __iter__, __next__, __enter__ et __exit__? Je me doute bien que ce n'est pas implémenté comme ça, mais j'essais de m'imaginer un modèle mental simple pour réfléchir à ces nouvelles constructions.

C'est ça.

Enfin, ça n'est pas spécifique à la version 3.5, mais je n'avais jamais vraiment vu d'exemples de asyncio. Ce n'est pas un peu redondant d'avoir des modules spécifiquement pour les opérations asynchrones (aiofiles, aiohttp)? Ne serait-il pas plus élégant de pouvoir écrire directement async with open(...) plutôt que d'aller chercher une version alternative de open?

Dans l'idée, ce serait bien si les objets builtins et les modules de la bibliothèque standard proposaient une interface asynchrone.

Dans la pratique, par contre, c'est pas toujours évident. En particulier, si on ajoute à la classe file les méthodes asynchrones qui vont bien et que l'on peut du coup utiliser la même classe dans ou en dehors d'asyncio, ça risque de rendre l'interface de cette classe un peu compliquée. En particulier parce qu'il n'est pas toujours facile de rendre asynchrone un code qui n'a pas été pensé comme ça au départ.

Du coup, c'est vrai que c'est redondant, mais il va falloir attendre un peu pour savoir si la communauté Python va travailler à rendre ces classes compatibles avec asyncio ou non. Ça représente quand même pas mal de boulot. :)

PS : Cela dit, sous Linux en particulier, on peut déjà utiliser des objets file tout à fait classiques avec asyncio, puisqu'il suffit d'utiliser les méthodes qui vont bien pour attendre que ceux-ci soient disponibles en lecture ou en écriture de façon asynchrone (ou plutôt dans le cas présent réagir avec un callback… ce qui est peu élégant). Les autres bibliothèques définissent justement des wrappers plus intuitifs à utiliser en se basant sur l'interface (relativement bas niveau) d'asyncio.

+1 -0

Merci à vous pour cet article très complet et bien expliqué :)

Juste quelques remarques/"coquilles":

  1. Résumé PEP 448 :
1
2
>>> def spam(a, b, c, d, e, g, h, i, j):
...    return a + b + c + d + e + f + g + h + i

Quelle est la raison qui vous à pousser à ne pas utiliser la même série de variables en entrée et en sortie pour la fonction spam() ? Ça m'a fait bloquer un moment ^^"

  1. Code explicatif de async, ligne 15 : manque un "r" à "response"
1
async for chunk in esponse.content.read_chunk(chunk_size):

Et sinon question de débutant : Quelle est la différence entre l'asynchrone et le temps partagé ? J'ai du mal à comprendre car les deux notions me semblent similaires : mettre en attente passive un processus lorsque celui-ci entame une entrée/sortie.

+0 -0

Merci pour le relevé des coquilles. J'ai envoyé une correction en validation.

Et sinon question de débutant : Quelle est la différence entre l'asynchrone et le temps partagé ? J'ai du mal à comprendre car les deux notions me semblent similaires : mettre en attente passive un processus lorsque celui-ci entame une entrée/sortie.

Giome

Par temps partagé, tu sous-entends thread ? Le cas échéant, j'avais posé la même question ici. ^^

+1 -0

Et sinon question de débutant : Quelle est la différence entre l'asynchrone et le temps partagé ? J'ai du mal à comprendre car les deux notions me semblent similaires : mettre en attente passive un processus lorsque celui-ci entame une entrée/sortie.

Giome

La différence, c'est qu'en programmation asynchrone, la suspension est explicite : tu décides toi-même quand une tâche peut être suspendue, il suffit de coller un yield. Le mécanisme de temps partagé qu'on retrouve sur les OS, en revanche, ne permet pas ça : les processus vont se suspendre à chaque interruption matérielle, mais comment savoir si une fonction donnée dans un code va engendrer ou non une interruption matérielle à moins de lire tout le code ?

+1 -0

Merci pour cet article très "nourrissant" ; je retiens principalement le développement d'applications et de scripts asynchrones qui seront plus faciles à écrire et à relire.

Le multicœur pourquoi pas, mais au final avec asyncio le CPU pourra être mieux exploité au lieu d'attendre bêtement ; il me semble que NodeJS, du temps où je l'avais essayé, n'exploitait qu'un seul cœur et s'en sortait très bien sur un grand nombre de requêtes.

Si le code métier est CPU-intensive autant l'interfacer en C/C++ avec du MPI ou OpenMP derrière pour utiliser les cœurs (voire un cluster) ; je ne vois pas très bien l'application du multicœur pour Python donc pour l'instant (mais peut-être que l'un d'entre vous éclairera ma lanterne).

+0 -0

Le multicœur pourquoi pas, mais au final avec asyncio le CPU pourra être mieux exploité au lieu d'attendre bêtement ; il me semble que NodeJS, du temps où je l'avais essayé, n'exploitait qu'un seul cœur et s'en sortait très bien sur un grand nombre de requêtes.

Si le code métier est CPU-intensive autant l'interfacer en C/C++ avec du MPI ou OpenMP derrière pour utiliser les cœurs (voire un cluster) ; je ne vois pas très bien l'application du multicœur pour Python donc pour l'instant (mais peut-être que l'un d'entre vous éclairera ma lanterne).

MicroJoe

Crée un pool de workers (intenses en CPU) qui bossent dans des processus séparés, fais les communiquer avec ton serveur (ou entre eux) au moyen de sockets Unix ou n'importe quelle IPC un peu performante (un Pub/sub ou une Queue dans redis ?) : un appel à un worker devient "une IO", donc tu peux paralléliser ta tâche, même si elle est CPU-bound, en la migrant vers un autre CPU. Et ce genre d'archi est complètement adaptable, que tu aies une machine de guerre à 26 CPU ou un cluster de 30 RaspberryPi sur un LAN.

Le principe des subinterpreters qui sont pressentis pour arriver dans Python 3.6 va remplacer ces processus séparés par des threads qui ne sont plus tributaires du GIL, mais qui peuvent quand même communiquer de façon efficace (ils ont quand même une mémoire partagée), donc ça rendrait ce modèle encore plus pertinent, et on pourrait faire de la programmation asynchrone et multi-coeur pour pas cher. :}

Edit : C'est pas forcément une révolution, mais sur certains types d'applis ça peut vraiment changer la donne en termes de design et de souplesse. En gros, c'est bien quand on veut faire une architecture orientée services (SOA).

+0 -0

Node.js utilise exactement le même principe que asyncio en fait. La seule vrai différence, outre le langage, est que node utilisé de callback tandis que asyncio préfère que les dev utilisent des coroutines (mais l'utilisation de callback est possible aussi)

Merci beaucoup pour cette explication limpide @nohar !

@Vayel : De rien ! Sinon, non, je parlais vraiment de temps partagé. Corrigez-moi si je me trompe mais on pourrait très bien avoir un système de threads sans que l'OS ne permette le temps partagé ? Les deux notions sont indépendantes.

Oui ça a déjà été signalé mais je n'ai pas de PC sous la main pour faire la correction :(

Sinon pour ceux que ça intéresse, quelques nouvelles de la mailing list :

  • Guido à inciter des dev à faire une pep informel sur git et github. Il semblerait qu'il soit plus ou moins motivé à y passer en lieu et place de mercurial et du dépôt auto-hébergé. Pour l'instant il s'agit surtout de documentation mais sachant que Guido utilise déjà github, entre autre, pour le développement de asyncio, il ne serait pas étonnant que Python finisse par y basculer. D'autant que github propose l'utilisation de mercurial.
  • Theo de Raadt (fondateur d'OpenBSD) à inciter Guido à lancer le sujet des générateurs de nombres aléatoire en Python. La doc précise bien que le générateur par défaut n'est pas sûrs au sens cryptographique du terme mais cela n'empêche pas certains développeurs de l'utiliser. Il semblerait que les développeurs seraient prêt à ajouter de nouveaux générateurs de nombre aléatoire et de faire migrer progressivement celui par défaut à un nouveau générateur plus sécurisé. Cela va passer par quelques dépréciations sur 2-3 versions. Cela n'est pas encore sûrs mais les développeurs charges de ce modules semblent motivé et Guido à les suivre.

Aucune idée. Peut être pour faire une transition plus douce ? bitbucket est bien connu des dev Python et utilise nativement mercurial. Toujours est il que l'un comme l'autre ne sont pas acté. Bitbucket n'a été utilisé qu'entre la rc1 et la finale, actuellement, pour la 3.6 ou la 3.5.1 les dev ont lieu sur le dépôt de la fondation. Pour git et github pour le moment il n'y a qu'une pep de doc. Aucune décision n'a été prise et aucun plan pour le moment ne prévoit le basculement.

Pensez-vous que le type hinting (PEP 484) pourrait améliorer les performances du langage ? (même si cela n'a pas l'air d'être l'objectif de cet ajout)

Gugelhupf

Indirectement, oui, ça peut.

Cython se sert de ce genre de hints (pas la même syntaxe, mais la même info) pour compiler certaines portions de code en C (utilisant l'API C de CPython), ce qui permet de faire des bonds en perfs. Par contre, à part dans certains cas particuliers (code très calculatoire), ces améliorations de perfs sont dues la plupart du temps au fait que ça va transformer des lookups dans des tables de hachage en accès directs à des pointeurs, donc j'imagine que ça ouvre une (très longue) route pour un JIT.

En tout cas ce n'est pas une hypothèse déconnante.

Plus directement (et sûrement à plus court terme), le type hinting va surtout aider au perfectionnement de vérifieurs de code statiques.

+0 -0

En théorie ça pourrait aider mais actuellement le type hinting n'a AUCUNE influence sur les perfs, rien n'est prévu en ce sens et Guido à semble relativement contre. Entre autre pour que ça reste facultatif, que personne ne soit forcé à l'utiliser et de toute façon actuellement les annotations peuvent servir a autre chose que l'indication de types

J'avais pas lu l'article à sa sortie. Je viens de le redécouvrir grâce aux zAwards.

Très bonne explication en ce qui concerne l'asynchrone. On aurait pu fusionner des explications communes avec ça. Et nohar semblait dire dans les commentaires qu'il préparait quelque chose sur le sujet. On pourrait vraiment s'y mettre à plusieurs, parce que c'est important de faire le tri entre coroutines, goroutines, node generators, … OS threading vs. green threading et en quoi c'est différent, etc.

En tout cas c'est de l'excellent travail. Félicitations.

+0 -0

Très bonne explication en ce qui concerne l'asynchrone.

C'est l'avantage d'avoir un débutant sur le sujet dans les auteurs. ^^

Et nohar semblait dire dans les commentaires qu'il préparait quelque chose sur le sujet.

Tout à fait : https://github.com/ArnaudCalmettes/article-async

En tout cas c'est de l'excellent travail. Félicitations.

Merci. :)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte