Proposition d’un mot clé pour l’évaluation paresseuse en Python 3.7


Si il y a bien une mailing-list à suivre, c’est Python-idea. Elle regorge de tout, on est y apprend sans cesse à propos de Python, la programmation en général, la gestion de communautés, etc. Mais c’est accessible pour peu qu’on soit à l’aise en anglais.

Parmi les sujets chauds du moment, il y a l’introduction, pour potentiellement Python 3.7, d’un mot clé pour évaluer paresseusement les expressions.

Je m’explique…

On a déjà plusieurs moyens de faire du lazy loading en python :

  • Le shortcut des opérateurs and et or. La partie droite ne s’exécute que si la gauche ne répond pas déjà à la question.
  • Les générateurs. Il ne sont évalués qu’au premier appel de next().
  • await : évalué seulement quand la boucle d’événement décide de passer par là.

La nouvelle proposition est quelque chose de différent : permettre de déclarer une expression arbitraire, mais qui n’est évaluée que la première fois qu’on la lit.

Ca ressemble à ça:

def somme(a, b):
    print('coucou')
    return a + b
 
truc = lazy somme(a, b)
print("Hello")
print(truc)

Ce qui afficherait:

Hello
coucou
3

On peut mettre ce qu’on veut après le mot clé lazy. Le code n’est exécuté qu’une fois qu’on essaye d’utiliser la variable truc.

L’usage essentiel, c’est de pouvoir déclarer du code sans chichi, comme si on allait l’utiliser maintenant. Le passer à du code qui va l’utiliser sans même avoir besoin de savoir que c’est un truc spécial. Et que tout marche à la dernière minute naturellement.

Par exemple, la traduction d’un texte dans un code Python ressemble souvent à ça :

from gettext import gettext as _
...
print(_('Thing to translate'))

Mais dans Django on déclare un champ de modèle dont on veut pouvoir traduire le nom comme ceci :

from django.utils.translation import ugettext_lazy
 
class Produit(models.Model):
    ...
    price = models.IntegerField(verbose_name=ugettext_lazy("price"))

La raison est qu’on déclare ce texte au démarrage du serveur, et on ne sait pas encore la langue dans laquelle on va le traduire. Cette information n’arrive que bien plus tard, quand un utilisateur arrive sur le site. Mais pour détecter toutes les chaînes à traduire, créer le fichier de traduction, construire le cache, etc., il faut pouvoir marquer la chaîne comme traductible à l’avance.

Django a donc codé ugettext_lazy et tout un procédé pour évaluer cette traduction uniquement quand une requête Web arrive et qu’on sait la langue de l’utilisateur.

Avec la nouvelle fonctionnalité, on pourrait juste faire:

from gettext import gettext as _
 
class Produit(models.Model):
    ...
    price = models.IntegerField(verbose_name=lazy _("price"))

Rien à coder nulle part du côté de Django, rien à savoir de plus pour un utilisateur. Ça marche dans tous les cas, pareil pour tout le monde, dans tous les programmes Python.

Bref, j’aime beaucoup cette idée qui permet de s’affranchir de pas mal de wrappers pour plein de trucs, mais aussi beaucoup aider les débutants. En effet les nouveaux en programmation font généralement des architectures basiques : pas d’injection de dépendances, pas de factories, etc. Avec lazy, même si une fonction n’accepte pas une factory, on peut quand même passer quelque chose qui sera exécuté plus tard.

Évidement ça ne dispense pas les gens de faire ça intelligemment et d’attendre des callables en paramètre. Dans le cas de Django, une meilleure architecture accepterait un callable pour verbose_name par exemple.

Mais c’est un bon palliatif dans plein de situations. Et l’avantage indiscutable, c’est que le code qui utilise la valeur paresseuse n’a pas besoin de savoir qu’elle le fait.

Les participants sont assez enthousiastes, et moi aussi, bien que tout le monde a conscience que ça pose plein de questions sur la gestion des générateurs, de locals(), et du debugging en général.

Plusieurs mots clés sont actuellement en compétition: delayed, defer, lazy. delayed est le plus utilisé, mais j’ai un penchant pour lazy.

Viendez sur la mailing list !

14 thoughts on “Proposition d’un mot clé pour l’évaluation paresseuse en Python 3.7

  • Poisson

    Du coup, des import lazy qui faciliterait l’utilisation de dépendance optionnelle? Ou complètement hors du contexte?

  • Rinrynque

    La mailing list de Python est peut-être bien mais pourquoi diable stocke-t-elle nos mots de passe en clair ?!

  • Sam Post author

    @Poisson je pense que c’est envisageable. Faut en parler sur la list avant qu’on commence l’implémentation car y a du boulot si les import doivent être gérés

  • bobdinar

    Python se met à l’asynchrone comme JS, Python se met au defer comme Golang…Bref on sent que Python n’est plus un leader.

  • Sam Post author

    Python n’a jamais été un leader et n’a rien inventé. Il a au contraire une longue tradition de récupérer les features testées et approuvées des autres langages. Parmi ses influences on compte ABC, ALGOL 68, C, C++, Haskell, Java, Lisp, Modula‑3 et Perl : generateurs, list comprehensions, lambda, map, decorateurs, metaclasses, regexes, indentations…

    Il n’y a donc rien d’étonnant que Python continue d’évoluer et s’inspire de l’expérience des langages contemporains pour s’améliorer. C’est un bon moyen de ne pas devenir obsolète sans avoir à implémenter des features expérimentales à l’avenir incertain.

    Comme d’habitude, la force de Python n’est d’être le meilleur nul part, mais très bon partout.

  • Moi

    Je ne comprends pas l’interet de lazy, ca peut etre fait avec lambda non ?

    def somme(a,b):
        print("coucou")
        return a+b
     
    truc = lambda : somme(1,1)
     
    print("hello")
    print(truc())

    =>

     
    hello
    coucou
    5
  • e-jambon

    L’évaluation paresseuse, ‘est un des aspect de ruby.

    L’un des intérêt c’est qu’une fois systématisé,on ne calcule que ce dont on a besoin (ou presque).

    Et ça offre son lot de surprises agréables.

    En particulier du point de vue de l’écriture du code.

    J’essaie d’illustrer en français :

    condition A ET condition B.

    => Si A est faux, pas la peine d’évaluer B, je sais déjà quoi retourner..

    Avec un minimum de rigueur, on peut économiser quelques calculs coûteux totalement inutiles.

    variable_a OU = 5

    En étant “paresseux” : si “variable_a” vaut false, pas la peine d’évaluer l’opérande de droite.

    En ruby, seuls nil et false sont évalués comme étant “faux”. Une variable qui n’a pas été affectée vaut nil.

    Donc je n’évalue la partie de droite QUE si variable_a n’a pas été affectée. Auquel cas, j’affecte la valeur 5 à variable_a.

    Au passage, ça me fait furieusement penser à un opérateur ternaire… sifflote

    Quant au ‘return’ lui même : en ruby, l’évaluation paresseuse est quasi systématique.

    Quand je rencontre un bloc qui dit “variable_a”, j’évalue ‘variable_a’.

    Tant qu’à évaluer, autant retourner la valeur, non ?

    En ruby, la dernière valeur évaluée, c’est la valeur de retour de la fonction. Parce que…tant qu’à faire d’être paresseux…

    Z’imaginez le nombre de fonction où ça vous évite d’écrire return ?

    Le nombre de raccourcis que ça offre ?

    Bref, j’espère sincèrement que ça se généralisera.

  • entwanne

    @e-jambon, Pas grand chose à voir avec l’évaluation paresseuse dans ce que tu dis, il n’y a d’ailleurs à ma connaissance rien de tel en Ruby.

    Ce que tu évoques en premier correspond aux shortcuts évoqués plus haut dans l’article, qui permettent de faire des expressions conditionnelles avec les mots-clefs and et or (qui restent des opérateurs binaires et non ternaires, ils ne prennent que deux opérandes).

    Quant au return, je pense que tu n’as pas compris le sens du mot « paresseux » dans « évaluation paresseuse ».

    Il ne s’agit pas au développeur d’être paresseux, mais de permettre d’évaluer une expression le plus tard possible.

    Au passage, le mot-clef return optionnel est loin d’être un avantage.

  • e-jambon

    @entwanne

    T’as 100% raison.

    J’ai très mal lu, en fait j’ai sauté deux paragraphes…J’ai été déconcentré … Toutes mes excuses, je n’ai pas vraiment relu avant d’envoyer le commentaire… C’était pour la bonne cause (la déconcentration).

    Ca m’apprendra à m’entêter quand on essaie activement de me déconcentrer…

  • Sam Post author

    @Moi : on peut, mais il faut que le code auquel tu passe la lambda attende un callable. lazy permet de passer une expression à un code qui n’attend pas un callable. Cela permet un desig de code très naturel: tu fais un code qui attend un string, et on te passe une string (avec lazy devant). Pas besoin de savoir si on va avoir que l’API prend un callaback avec x paramèters. Pas besoin de créer cette API.

    Accepter un callable pour absolument tout que potentiellement plus tard tu veux permettre d’injecter peut être un travail énorme, et surcharger le code. En général on le fait au fur et à mesure que l’API muri. lazy permet en attendant de faire le bouche trou.

    L’exemple typique qui a lancé la discussion sur la mailing list est le module de logging:

    log.message(‘Ceci est un %(bar)s’, {‘bar’: foo()})

    Le problème de ça c’est que quoi qu’on fasse on doit appeler foo(), et si c’est un appel long, chaque appel de log est long. Mais si on décide de désactiver ce log level, on voudrait que les messages ne s’appliquent pas.

    Idéalement, il aurait fallut que l’API de log accepte un callable en second paramètre. Néanmoins ce n’est pas le cas, et comme l’API est dans la lib standard, elle ne va pas changer de si tôt. Lazy permettrait de toujours s’en sortir dans des cas comme ça.

    D’une manière générale c’est aussi un shortcut agréable. Je peux m’imaginer en train de scripter et me dire “je vais dropper des lazy” plutôt que de me faire chier à mettre des callback pour un code qui sera de toute façon jetable.

  • e-jambon

    Okey j’ai dis de la merde …

    Mais tant qu’à nager dedans : lazy est présent dans ruby depuis la v2.

    Et pis je parlais d’opérateur ternaire pour ||= , ou de &&=. 1 condition à gauche , une condition à droite, laquelle deuxième condition s’avère être une affectation. 2 opérations, 3 opérandes, un opérateur. C’est bien un opérateur ternaire ça, non ?

    Quant à return je nage pas, je coule….

  • entwanne

    Mais tant qu’à nager dedans : lazy est présent dans ruby depuis la v2.

    La nouvelle méthode lazy des Enumerable ?

    Ça revient aux générateurs de Python, c’est une manière d’avoir un comportement paresseux, mais ça n’est pas applicable à autre chose qu’aux itérables (énumérables en ruby).

    On est ici dans l’idée d’un opérateur lazy qui pourrait s’appliquer à toute expression.

    Et pis je parlais d’opérateur ternaire pour ||= , ou de &&=. 1 condition à gauche , une condition à droite, laquelle deuxième condition s’avère être une affectation. 2 opérations, 3 opérandes, un opérateur. C’est bien un opérateur ternaire ça, non ?

    Je vois bien les deux opérations (résumées en un seul opérateur), et on peut tirer par les cheveux pour faire apparaître 3 opérandes (dont un dupliqué) en disant que a ||= b est équivalent à a = a || b, mais ça reste pour moi un opérateur binaire.

Comments are closed.

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