Perl / Python / Ruby

Le problème exposé dans ce sujet a été résolu.

nonlocal a été rajouté en 3.0 (si je ne dis pas de bêtise) entre autres pour pouvoir écrire la deuxième fonction sans hack sale, mais ce problème ne devrait déjà pas se poser (et ça fait hack sale malgré tout). Pouvoir casser un code correct qui fonctionne parfaitement en rajoutant une affectation de variable plus tard, c'est quand même gênant.

Je ne suis pas vraiment d'accord.

tl;dr: Cela vient du fait qu'en Python la portée d'un symbole doit être unique dans un code object, elle ne peut pas varier dans le temps.

La portée des symboles d'un code object ne doit jamais être ambiguë. Dans ton second exemple, ça l'est, parce qu'il y a une chance sur 2 que ce code ne fasse pas ce que tu veux.

  • Soit tu veux que a soit une variable libre (celle qui vient de l'environnement de f), auquel cas ta seconde ligne doit vouloir dire "je mets mon a à jour après l'avoir affiché", ce qui n'est pas le cas puisque l'opérateur d'affectation, sans indication explicite supplémentaire, crée systématiquement un lien dans le scope local, c'est sa définition.

  • Soit tu veux que a soit une variable locale à l'environnement de g et tu cherches à l'afficher avant qu'elle existe auquel cas c'est une erreur qui doit péter à la compilation.

Le compilateur ne peut pas deviner ton intention, et d'ailleurs il ne le fera jamais (in the face of ambiguity, refuse the temptation to guess). Si tu veux modifier le binding d'une variable libre dans le code object, il faut le dire explicitement parce que l'opérateur = ne veut pas le deviner. D'où le mot-clé nonlocal qui résoud le problème en respectant le zen de Python : explicit is better than implicit.

+1 -0

J'ai l'impression que tu me réponds en disant « je ne suis pas d'accord, c'est normal que le deuxième exemple renvoie une exception ». Si on met de côté le fait que ça devrait être fait statiquement plutôt qu'à l'exécution (ce avec quoi tu as l'air d'être d'accord aussi), ça ne répond pas à ma critique : je ne dis pas « la portée est mal foutue parce que ce code pète », je dis « la portée est mal foutue parce que ces deux codes ont un comportement radicalement différent alors qu'ils ne devraient pas ».

Je n'ai pas de solution miracle pour ça, mais ça n'en est pas moins un problème, qui n'apparaît par exemple pas du tout dans des langages où la portée est explicite (comme les ML). Le fait que ce soit « par définition » ou « dans le zen de Python » n'est pas une circonstance atténuante, mais plutôt une indication que c'est un problème intrinsèque de conception du langage.

+1 -0

J'ai l'impression que tu me réponds en disant « je ne suis pas d'accord, c'est normal que le deuxième exemple renvoie une exception ». Si on met de côté le fait que ça devrait être fait statiquement plutôt qu'à l'exécution (ce avec quoi tu as l'air d'être d'accord aussi), ça ne répond pas à ma critique : je ne dis pas « la portée est mal foutue parce que ce code pète », je dis « la portée est mal foutue parce que ces deux codes ont un comportement radicalement différent alors qu'ils ne devraient pas ».

Eusèbe

Non en fait je te réponds : "en vertu de quoi ils ne devraient pas ?".

Pour moi ce n'est pas un problème de conception : la portée d'une variable doit être unique à un code object, et non-ambiguë. Or les règles qui régissent la portée de ces variables sont très précises, c'est justement au travers de l'opérateur d'affectation (et des mots-clé global et nonlocal qui servent à préciser son comportement) que ça se passe.

Ce que tu appelles un problème de conception, c'est simplement le fait que ce soit cet opérateur (sa présence) qui prévale pour définir la portée d'une variable dans tout le code object (donc un comportement statique), plutôt qu'un élément parfaitement dynamique comme "le scope dans lequel on retrouve la variable a la première fois qu'on y accède".

+0 -0

Non en fait je te réponds : "en vertu de quoi ils ne devraient pas ?".

En vertu d'un principe qui devrait relever du bon sens qui dit que faire des modifications locales dans un code ne devrait pas modifier la sémantique de lignes qui n'ont a priori rien à voir et qui sont en plus situées avant. Je conçois bien que « pour toi » ce n'est « pas un problème » parce que « la portée d'une variable doit être unique à un code object » (je pourrais te retourner la question : en vertu de quoi ?), mais c'est très contestable (et même si c'est un vil argument d'autorité, c'est une critique assez répandue parmi les gens dont le métier est de réfléchir à qu'est-ce qu'un langage correctement conçu).

Pour moi ce n'est pas un problème de conception : la portée d'une variable doit être unique à un code object, et non-ambiguë.

Peut-être que c'est le principe de code object qui est la source du problème alors.

Ce que tu appelles un problème de conception, c'est simplement le fait que ce soit cet opérateur (sa présence) qui prévale pour définir la portée d'une variable dans tout le code object (donc un comportement statique), plutôt qu'un élément parfaitement dynamique comme "le scope dans lequel on retrouve la variable a la première fois qu'on y accède".

Oui, c'est effectivement un problème que l'utilisation locale de cet opérateur ait une influence globale sur tout le code qui se situe au même niveau d'indentation. J'ai bien compris tes arguments, mais ils prennent le problème à l'envers : tu commences par considérer quelques règles fondamentales de la sémantique comme bonnes a priori, puis en partant de ces règles tu déduis que ce comportement est le seul possible, donc qu'il ne pose pas de problème. Je dis ça sans aucune animosité ni condescendance, mais ce n'est pas très honnête comme raisonnement. Il serait plus correct de dire que c'est un comportement mauvais parce qu'il rend le code très peu robuste à une modification locale et qu'il demande d'avoir une vision détaillée de tout ce qui se passe dans un bloc pour savoir si une ligne précise est correcte ou non, et si ce comportement mauvais est le seul possible, c'est que les causes pour lesquelles il est nécessaire sont elles-mêmes problématiques. Est-ce que ça pose problème en pratique ? Je ne sais pas, ça fait bien longtemps que je ne fais quasiment plus de Python. Mais c'est tout de même un défaut du langage.


En remarque vaguement liée qui n'appelle pas de réponse (parce que ce serait un débat vraiment sans intérêt - et que je sais déjà que les gens que je cible ne se reconnaissent pas) et qui ne te vise pas personnellement, je ne comprends pas pourquoi certaines personnes ont autant de mal à accepter que leur langage préféré a des défauts. S'il existe des gens dont le métier est de réfléchir à la conception de langages meilleurs, c'est précisément parce que ceux qu'on a actuellement ne sont pas parfaits, et reconnaître quand il y a un problème est une attitude bien trop peu répandue par rapport à celle qui veut le cacher sous le tapis en invoquant des raisons un peu arbitraires pour la justifier.

Autre remarque qui n'appelle pas non plus de réponse (mais qui te vise :P ) : statique, c'est quand c'est détecté avant l'exécution. On veut bien être gentil et appeler les tags des valeurs en Python des types pour faire semblant que c'est pareil, mais dire que lever une exception est un comportement statique, c'est un peu pousser mémé dans les orties :-°

+1 -0

Autre remarque qui n'appelle pas non plus de réponse (mais qui te vise :P ) : statique, c'est quand c'est détecté avant l'exécution. On veut bien être gentil et appeler les tags des valeurs en Python des types pour faire semblant que c'est pareil, mais dire que lever une exception est un comportement statique, c'est un peu pousser mémé dans les orties :-°

Il y aura quand même une réponse parce que tu n'as pas compris cette phrase.

Je dis juste que définir la portée d'une variable en se basant sur la présence de l'opérateur d'affectation dans le bloc, revient à définir la portée sur un critère statique, et je l'ai précisé entre parenthèses pour lui opposer le critère dynamique que j'évoque dans la seconde proposition de la même phrase.

Tout va bien, donc, mon vocabulaire est sauf.

PS : Par contre on ne doit pas avoir la même définition de tag parce que les objets Python pointent directement sur un noeud de l'arborescence de types, donc sur leur type lui-même, et ne portent pas de tag pour représenter cette info.

+0 -0

Très bien :D

Il y aura quand même une réponse parce que tu n'as pas compris cette phrase.

Je dis juste que définir la portée d'une variable en se basant sur la présence de l'opérateur d'affectation dans le bloc, revient à définir la portée sur un critère statique, et je l'ai précisé entre parenthèses pour lui opposer le critère dynamique que j'évoque dans la seconde proposition de la même phrase.

Non. Malgré les apparences, je ne suis pas complètement idiot et je comprends bien ce que tu veux dire, mais ce n'est pas parce que = définit une portée sur tout le bloc (donc qui pourrait être détectée à la compilation) que c'est statique. Statique, ça veut dire « avant l'exécution/à la compilation ». Ici, l'erreur est détectée et déclenchée à l'exécution, donc c'est dynamique. C'est aussi simple que ça. Il y a un moyen simple de le voir : si tu enlèves l'appel à g(), tout se passe bien, parce que c'est uniquement quand le code qui pose problème est exécuté qu'il pose problème. Si c'était un critère statique, l'exécution de f sans g renverrait une erreur. Mais comme le code de g n'est pas exécuté (indice : ce mot fait-il penser à statique ou à dynamique ?), ce n'est pas le cas.

C'est aussi une mode assez répandue d'utiliser des mots qui ont déjà un sens précis pour leur en donner un qui a un rapport extrêmement vague en prétendant qu'il s'agit de la même chose, mais je pense que c'est une erreur (et je fais partie des gens pour lesquels l'informatique devrait être plus considérée comme une science et pas une simple technique, donc je pense aussi que c'est un réel problème).

Par contre on ne doit pas avoir la même définition de tag parce que les objets Python pointent directement sur un noeud de l'arborescence de types, donc sur leur type lui-même, et ne portent pas de tag pour représenter cette info.

Plus exactement, on n'a pas la même définition de type, parce que des gens ont un jour décidé d'utiliser ce mot qui désigne une notion purement syntaxique et statique pour parler d'étiquettes (ou tags) attachées dynamiquement aux valeurs du langage (peu importe l'implémentation exacte) et qui servent à vérifier à l'exécution qu'on ne fait pas trop de bêtises. On peut comprendre la confusion, mais les deux notions n'ont rien à voir, malgré les apparences. Mais bon hein, c'est entré dans les usages, et je n'ai pas envie de me battre contre un moulin, alors si ça te fait plaisir, pourquoi pas parler de types pour Python.

Edit : un autre code qui met encore plus en évidence le fait que ce soit un comportement dynamique :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>> a = 3
>>> def f():
...     if 0:
...             print(a)
...     a = 4
... 
>>> f()
>>> def f():
...     if 1:
...             print(a)
...     a = 4
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f
UnboundLocalError: local variable 'a' referenced before assignment

Donc selon qu'une condition soit valide ou pas à l'exécution, toute la sémantique de mon programme est radicalement changée. C'est à la fois purement dynamique (on ne se pose pas la question de la liaison du a dans le print avant d'essayer d'exécuter cette instruction) et un défaut du langage.

+0 -0

Dans ton post tu sembles confondre critère statique avec «  qui fait l'objet d'une vérification statique » ou plus grossièrement « déclencher une erreur de compilation ». Et c'est là qu'à mon avis tu ne comprends pas ce que je dis.

En Python, la portée des variables (leur portée, pas leur existence) est résolue au moment où le compilateur choisit l'opcode avec lequel il va charger un symbole donné sur la pile, tout simplement parce que suivant que ce symbole est une variable libre, locale au code object, ou bien une cellvar, bref, suivant la portée de la variable, les opcodes que le compilateur choisit pour la manipuler ne sont pas les mêmes.

Choisir les opcodes, on est bien d'accord que ça se passe à la compilation lors de la production du bytecode, non ?

Donc puisque ce critère est connu et utilisé lors de la compilation, il est statique, et c'est exactement de ça que je parle quand je dis que la portée des variables est définie en se basant sur un critère statique.

Mais ça ne veut pas dire qu'il y a une vérification statique sur l'existence des variables, ou plutôt sur le fait qu'elles soient bindées avant d'être utilisées. Du moins l'interpréteur standard ne fait pas cette vérification (mais pylint, oui, ainsi que d'autres outils de vérification statique).

Edit: on me signale dans l'oreillette que le ton de mon message était inapproprié. Je m'en excuse. Je viens de le corriger.

+0 -0

Je reprends la phrase originale :

Ce que tu appelles un problème de conception, c'est simplement le fait que ce soit cet opérateur (sa présence) qui prévale pour définir la portée d'une variable dans tout le code object (donc un comportement statique), plutôt qu'un élément parfaitement dynamique comme "le scope dans lequel on retrouve la variable a la première fois qu'on y accède".

Effectivement, j'ai mal compris ce que tu voulais dire. Mais c'est alors parce que j'avais supposé que tu avais toi-même bien compris ma critique, ce qui n'est en fait pas le cas : évidemment que la liaison statique est le seul choix raisonnable. Le problème n'est pas lié à ce choix de principe (finalement assez vague) mais à la façon dont il est implémenté dans le langage (à savoir de façon très peu robuste, et qui permet d'écrire trop facilement des codes qui ne font pas du tout ce qu'on attend).

(Aussi, pour la suite de ma défense, le fait d'utiliser « comportement » au lieu de « critère » rendait cette phrase très peu claire - mais je te reconnais que tu as parlé de critère dans ton deuxième message).

Edit : la remarque sur pylint est intéressante, mais je ne pense pas qu'il soit pertinent de parler d'outils externes comme d'un critère utile (ce que tu ne fais pas) dans ce genre de discussion sur la conception d'un langage (même si c'est très tentant, et que c'est un point de vue que j'admets au moins discutable).

+0 -0

Je reconnais que ma phrase initiale portait à confusion.

Le problème n'est pas lié à ce choix de principe (finalement assez vague) mais à la façon dont il est implémenté dans le langage (à savoir de façon très peu robuste, et qui permet d'écrire trop facilement des codes qui ne font pas du tout ce qu'on attend).

Je ne suis vraiment pas assez calé sur le domaine pour avoir un avis complètement tranché, mais j'ai quand même l'impression que ce choix (c'est la présence de l'opérateur d'affectation qui définit la portée des variables dans tout le code object) est le seul choix à peu près sain qui puisse être fait dans un langage dynamique comme Python, et qui permette de supporter quand même des closures. Ce qui est déroutant, c'est probablement le fait que l'opérateur = a un sens plus subtil qu'on ne l'imagine en Python et que ce n'est pas forcément intuitif pour tout le monde.

En fait, la légère digression sur pylint me fait comprendre que tu reproches à python de ne pas faire surgir une erreur lors de la compilation de la fonction mais à l'exécution. Le fait est que rien n'empêche techniquement l'interpréteur de faire une vérification statique sur ce comportement (puisque les outils d'analyse statique savent le faire — je viens quand même de vérifier avec pylint —), donc est-ce vraiment un défaut du langage ou une feature manquante de l'interpréteur standard ?

Au final, je comprends ton point de vue, je concluerai simplement en disant que dans la réalité, ce détail ne m'a jamais posé de problème.

Edit : Quoique, on peut reconnaître ça à Perl : il permet de définir explicitement la portée des variables avec les mots-clé my et our… même si le langage ne l'impose pas par défaut, et permet de faire absolument n'importe quoi à ce niveau.

+0 -0

Je ne suis vraiment pas assez calé sur le domaine pour avoir un avis complètement tranché, mais j'ai quand même l'impression que ce choix (c'est la présence de l'opérateur d'affectation qui définit la portée des variables dans tout le code object) est le seul choix à peu près sain qui puisse être fait dans un langage dynamique comme Python, et qui permette de supporter quand même des closures. Ce qui est déroutant, c'est probablement le fait que l'opérateur = a un sens plus subtil qu'on ne l'imagine en Python et que ce n'est pas forcément intuitif pour tout le monde.

Ce n'est pas strictement lié au fait que Python soit un langage dynamique ou pas : Lisp par exemple n'a pas ce problème, et tu pourrais prendre un OCaml sans type et ne pas l'avoir non plus. D'ailleurs, comme tu l'as fait remarquer (:P ), la liaison est statique en Python.

Cela dit, si on remplace « dynamique » par « en gardant le reste de Python », je ne sais pas quel serait le meilleur choix. Mais encore une fois, c'est plutôt un symptôme que « le reste de Python » a des problèmes de fond (et je pense par ailleurs que le fait d'être aussi dynamique est plus un gros inconvénient qu'un avantage).

En fait, la légère digression sur pylint me fait comprendre que tu reproches à python de ne pas faire surgir une erreur lors de la compilation de la fonction mais à l'exécution. Le fait est que rien n'empêche techniquement l'interpréteur de faire une vérification statique sur ce comportement (puisque les outils d'analyse statique savent le faire — je viens quand même de vérifier avec pylint —), donc est-ce vraiment un défaut du langage ou une feature manquante de l'interpréteur standard ?

Je ne suis pas très convaincu par la rhétorique « ce n'est pas le langage le problème, c'est l'implémentation ». Je pense que les deux vont ensemble et qu'un défaut de l'interpréteur standard, dans un langage comme Python où il représente l'implémentation de référence (et l'écrasante majorité des cas d'utilisation), un défaut de l'un est un défaut de l'autre.

S'il y avait une sémantique proprement définie quelque part et que l'interpréteur standard indiquait explicitement que ce n'est qu'une implémentation parmi d'autres qui ne respecte pas forcément cette sémantique à la lettre, alors oui, on pourrait dire que c'est une feature manquante de l'interpréteur. Mais ce n'est pas le cas, et ça n'empêche pas les outils d'analyse statique d'aller plus loin.

Par ailleurs, je reproche effectivement à Python d'avoir un comportement dynamique sur beaucoup de choses où une vérification statique serait meilleure. Mais encore une fois, ce n'est pas le problème que je soulève ici : que ce soit vérifié statiquement (ce qui serait préférable) ou pas, je critique le fait que les règles de portée font qu'une modification locale raisonnable puisse avoir des conséquences sur des bouts de code qui n'ont rien à voir, simplement parce qu'ils sont syntaxiquement situés au même niveau d'indentation.

On peut aussi critiquer le fait que ces règles ne soient pas composables :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> a = 5
>>> def g():
...     a = 3
...     print(a)
...
>>> def f():
...     print(a)
...     g()
...
>>> f()
5
3
>>>

Si finalement je décide de rentrer la définition de g à l'intérieur de f, ou même d'inliner l'appel, boum, c'est cassé.

Edit : l'équivalent en OCaml (où j'ai mis des in partout pour que ça se voit mieux et que ça soit plus évident de voir comment composer) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let () =
  let a = 5 in

  let g () =
    let a = 3 in
    print_int a
  in

  let f () =
    print_int a;
    g ()

  in f ()

Ce code affiche 53. On peut définir g dans f, on peut l'inliner, la sémantique reste parfaitement claire et identique.

+0 -0

Là où tu vois de la rhétorique dans mon post je vois un grossier amalgame dans le tien : CPython n'est pas la seule implémentation de Python, donc tu confonds volontairement le langage et l'implémentation, et c'est au tour de ton argumentaire sur l'existence ou non d'une référence du langage séparée de la doc de son implémentation standard de ne pas me convaincre : techniquement les deux existent sur le même site (docs.python.org) et sont clairement distincts l'un de l'autre.

Ensuite tu reformules ton reproche en disant "je ne peux pas déplacer mon code avec la garantie qu'il compilera pareil" et tu me sors un exemple avec une closure qui lit une variable globale là où 99 % des développeurs Python écriraient une classe par réflexe pour obtenir la même chose (maintenir un état muable lié à des méthodes). En fait, tu reproches à Python d'avoir un comportement pas très joli et d'imposer une syntaxe plus lourde quand tu veux l'utiliser de façon non idiomatique. C'est un peu comme si je reprochais à OCaml sa syntaxe insupportable pour faire de la POO.

De plus, pour que cet exemple foire et pouvoir vérifier dans un analyseur statique qu'il était détecté, il a fallu que j'ignore les 15 autres erreurs que pylint ou flake8 sortent sur ce code. En particulier les noms de variables et de fonction ne sont pas explicites et ne font qu'un seul caractère de long. Et c'est là où j'en arrive.

Dans la vraie vie, ce cas ne sera finalement jamais amené à se produire :

  • d'abord parce que le cas d'utilisation que tu montres repose sur la confusion entre une variable locale et une variable globale qui portent le même nom. Pour peu que tu aies nommé tes variables de façon explicite dans ton code, tu peux copier-coller ce que tu veux où tu veux, ce clash a une probabilité quasi-nulle. Et il est même impossible si tu suis la coding-style standard qui veut que les variables globales soient écrites en majuscules.
  • Et encore, supposons qu'il arrive quand même. Tu me dis que les outils externes ne sont pas pertinents de la discussion : je suis convaincu du contraire. Dans n'importe quel projet sérieux, à notre époque, tout code passe au travers d'un système d'intégration continue avant d'être lâché dans la nature, et même avant ça, il passe dans un analyseur statique dans un hook de ton éditeur de texte ou de ton gestionnaire de version avant même que le fichier ne soit sauvegardé : si je tape le code que tu me montres dans vim, il me surligne directement la ligne incriminée en rouge.

Alors ok, je t'accorde que l'interpréteur Python devrait faire cette vérification par principe, mais la réalité des utilisateurs de ce langage fait que son absence n'est pas pénalisante, parce que de nos jours, ce style d'erreur est détecté avant même que le code ne soit enregistré, et parce qu'il faut vraiment le faire exprès pour la reproduire.

D'un point de vue purement théorique, ce n'est peut-être pas entendable, mais vu que ce sujet parle de langages de scripts, il me semble raisonnable d'accepter l'idiome "practicality beats purity".

+1 -1

Je profite de ce topic pour poser des questions un peu HS concernant python et sa comparaison avec d'autres langages, j'espère que vous ne m'en voulez pas trop.

Un truc qui m'a étonné en python alors que beaucoup d'autres langages à typage dynamique font exactement l'inverse, c'est la distinction entre objet.XXX et objet['XXX']. Ca m'a déjà joué des tours quand dans beaucoup d'autres langages que je connais les deux signifient exactement la même chose.

Pour vos histoires de portée de variables, je n'ai pas tout suivi, mais c'est assez rigolo de voir que tous les langages abordent cette question d'une façon assez différente :

  • Python: local par défaut sauf si on précise global
  • Lua: global par défaut sauf si on précise local
  • JavaScript: global si on ne précise rien, il faut var/let/const pour avoir une variable locale au bloc ou à la fonction
  • PHP: local par défaut sauf si on précise global; dans les closures il faut explicitement indiquer les variables qu'on veut capturer (et je me demande bien pourquoi)
  • Ruby: dans mes souvenirs, la portée locale ou globale est uniquement indiquée par une convention du langage, un caractère $ ou rien
  • Perl: je me souviens de mots-clés my et our ou rien mais c'est tellement confus et abscons que c'en est incompréhensible, et en plus c'est mélangé avec des notions de références et de copies implicites ou pas selon les cas, si je me rappelle bien

J'ai une autre question, les fonctions en python sont-ils vraiment des objets de première classe jusqu'au bout ? Certes on peut passer une fonction comme paramètre à une autre fonction, mais :

  • JavaScript: f = function (...) { ... };
  • PHP: $f = function (...) use (...) { ... };
  • Lua: f = function (...) ... end
  • Python: f = def (…): … ` = erreur de syntaxe ? Pourtant ça me paraissait assez logique ?

J'aime beaucoup la distinction string vs buffer que python fait, et que JavaScript tend à faire aussi. A l'inverse de PHP qui essaie de le faire un peu mais n'importe comment, ruby seulement à moitié comme déjà dit, ou plus étonnament perl, qui ne font pas la distinction. En 2016 ça devient un prérequis pour un bon langage de script. Venant de perl je trouve que c'est dommage, sachant qu'il est historiquement le pionnier des regexp et des traitements avancés sur les chaînes de caractères.

Comme pas mal d'autres avis je crois que perl est bon pour les oubliettes, il n'a plus rien de sexy ou d'intéressant à part pour maintenir des projets déjà existants dans ce langage; comme Pascal ou Fortran vs C/C++.

Sinon, on devrait ajouter JavaScript dans ce débat, JavaScript dans sa version Node.js évidemment; parce que ça peut faire pas mal de choses assez efficacement mine de rien. JE suis d'avis que ça peut être un concurrent sérieux en tout cas. On peut faire du web avec un truc ultra-simple comme express ou quelques frameworks plus complets; il n'y a qu'à voir gulp pour se convaincre que pour des petits scripts système ça passerait très bien; sur le plan paradygme fonctionnel c'est bien fourni… c'est le bordel dans la portée des variables comme ailleurs par contre.

EDIT: à cause de cet #*@%! de markdown qui exige une ligne blanche avant une liste

+0 -0

Un truc qui m'a étonné en python alors que beaucoup d'autres langages à typage dynamique font exactement l'inverse, c'est la distinction entre objet.XXX et objet['XXX']. Ca m'a déjà joué des tours quand dans beaucoup d'autres langages que je connais les deux signifient exactement la même chose.

Tiens c'est marrant, moi c'est l'inverse. Je trouve la logique de python plus clean. Probablement une question d'habitude.

J'ai une autre question, les fonctions en python sont-ils vraiment des objets de première classe jusqu'au bout ? Certes on peut passer une fonction comme paramètre à une autre fonction, mais :

  • Python: f = def (…): … ` = erreur de syntaxe ? Pourtant ça me paraissait assez logique ?

Oui les fonctions python sont vraiment des objets de première classe. La différence est que le langage n'autorise pas quels soient anonymes. La déclaration de fonction en python est une instruction et pas une expression. Le fait que tu ne puisse pas faire des fonctions anonymes de ce type ne change pas le fait que la fonciton, une fois définit, soit un objet pur et dur.

J'ai une autre question, les fonctions en python sont-ils vraiment des objets de première classe jusqu'au bout ? Certes on peut passer une fonction comme paramètre à une autre fonction, mais :

  • Python: f = def (…): … ` = erreur de syntaxe ? Pourtant ça me paraissait assez logique ?

Oui les fonctions python sont vraiment des objets de première classe. La différence est que le langage n'autorise pas quels soient anonymes. La déclaration de fonction en python est une instruction et pas une expression. Le fait que tu ne puisse pas faire des fonctions anonymes de ce type ne change pas le fait que la fonciton, une fois définit, soit un objet pur et dur.

Kje

Par rapport à ce qu'a dit Kje, j'ajouterais que c'est un choix volontaire de Python d'imposer cette contrainte : there should be one, and preferably only one, obvious way to do it. Ici, ça veut dire qu'une définition de fonction nommée ne peut être faite que via la syntaxe def func_name (*args):. Si la syntaxe était autorisée, ça serait strictement équivalent à ton exemple, mais pourquoi vouloir définir une fonction et la binder immédiatement à une variable avec le symbole = alors que c'est très exactement ce que fait la syntaxe normale ?

Python part du principe que si ta fonction doit être nommée, elle doit être déclarée normalement, par souci de cohérence et de lisibilité. Par contre il t'autorise à déclarer une fonction dans n'importe quel scope.

Pour la même raison, tous les analyseurs statiques râleront devant une expression du style fonction = lambda *args: ..., pas parce que tu utilises une lambda, mais parce que tu utilises une lambda pour la nommer dès le départ. Enfin, la syntaxe lambda est volontairement restreinte à des fonctions qui consistent en une seule expression, parce que pour tous les autres cas d'utilisation, Python t'encourage à créer une fonction normale séparément de l'endroit où tu la manipules.

La différence que tu observes est donc juste un choix syntaxique, assumé par Python, pour renforcer ce que le langage considère être une best practice.

+0 -0

J'ai une autre question, les fonctions en python sont-ils vraiment des objets de première classe jusqu'au bout ? Certes on peut passer une fonction comme paramètre à une autre fonction, mais :

  • JavaScript: f = function (...) { ... };
  • PHP: $f = function (...) use (...) { ... };
  • Lua: f = function (...) ... end
  • Python: f = def (…): … ` = erreur de syntaxe ? Pourtant ça me paraissait assez logique ?

QuentinC

Un nom de fonction (ou un appel de fonction) sont des expressions en Python. Un bloc de définition de fonction n'en est pas une, ce qui explique l'erreur de syntaxe.

Comme indiqué, ça ne change rien au fait que les fonctions soient des objets de première classe. Les entiers aussi sont des objets de première classe, pourtant tu ne peux pas assigner à une variable la définition d'un autre entier (a = (b = 0)).

Sinon, on devrait ajouter JavaScript dans ce débat, JavaScript dans sa version Node.js évidemment; parce que ça peut faire pas mal de choses assez efficacement mine de rien. JE suis d'avis que ça peut être un concurrent sérieux en tout cas.

QuentinC

Dans un langage dédié au script, il faut considérer la diversité de la bibliothèque standard et donc ce qu'il est possible de faire de base avec le langage. Je ne pense clairement pas que Javascript joue dans la même cour.

Il y a quand même un trou dans votre analyse d'après moi.

D'accord, mon f = def (*args): ... serait exactement identique à une déclaration normale def f (*args): ... . La philosophie de python veut qu'il ne puisse pas y avoir deux façons similaires d'écrire la même chose, soit. En passant c'est rigolo l'antinomie des slogans de python et de perl.

Mais alors pourquoi le langage n'a pas prévu de fonctions anonymes de plus d'une instruction ? Dans certains cas je pense que ça peut être vachement utile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
functions = {}

def call (name, a, b):
  return functions[name](a,b)


fonctions['add'] = lambda a, b: a+b

fonctions['mul'] = lambda a, b: a*b

functions['max'] = def (a, b):
  if a>b: return a
  else: return b

call('max', 7, 12)

OK, mon exemple est hautement débile, on a une fonction built-in max. ET peut-être même doublement débile si l'opérateur ternaire existe en python (apparament pas sauf en trichant avec condition and true_expr or false_expr` qui ne fait probablement pas pareil) avec des tests sur None. Mais je pense que vous voyez quand même l'idée…

Dans un exemple comme celui-là, quel est l'intérêt de déclarer la fonction avec un nom à la con genre ___tmp123 (parce qu'on est bien obligé de choisir un nom inutile), et seulement l'assigner ensuite, au lieu de le faire directement ?

Les entiers aussi sont des objets de première classe, pourtant tu ne peux pas assigner à une variable la définition d'un autre entier (a = (b = 0)).

Si… si on enlève les parenthèses ça marche très bien.

Dans un langage dédié au script, il faut considérer la diversité de la bibliothèque standard et donc ce qu'il est possible de faire de base avec le langage. Je ne pense clairement pas que Javascript joue dans la même cour.

Pas dans la même cour ? Avec tout ce qu'on peut trouver sur npm ? JE suis sûr qu'il y a autant de bordel que dans pip. Pour à peu près tout et n'importe quoi il y a toujours un package qui existe déjà. Bon c'est quand même très différent, ça dépend beaucoup de ce qu'on veut faire en fait. Mais il y a autant à fouiller je pense.

+0 -0

OK, mon exemple est hautement débile, on a une fonction built-in max. ET peut-être même doublement débile si l'opérateur ternaire existe en python (apparament pas sauf en trichant avec condition and true_expr or false_expr` qui ne fait probablement pas pareil) avec des tests sur None. Mais je pense que vous voyez quand même l'idée…

QuentinC

L'opérateur conditionnel ternaire existe en Python, et il se s'agit pas de tricher : a if a > b else b.

Dans un exemple comme celui-là, quel est l'intérêt de déclarer la fonction avec un nom à la con genre ___tmp123 (parce qu'on est bien obligé de choisir un nom inutile), et seulement l'assigner ensuite, au lieu de le faire directement ?

QuentinC

Une fonction de plus d'une expression est considérée comme assez complexe et mérite donc un nom à part entière. Dans tous les cas, lorsque tu la passeras en paramètre à un autre objet, elle possédera un nom, pourquoi ne pas lui assigner directement ?

Comme indiqué par nohar il me semble, tu n'es pas obligé de créer ta fonction dans le scope global, et donc la portée de son nom ne sera valide que pour le scope courant.

Sans oublier, dans ton exemple, que l'indentation deviendrait alors affreuse.

Si… si on enlève les parenthèses ça marche très bien.

QuentinC

Non. Tu parles là d'une fonctionnalité du langage qui est de pouvoir chaîner les opérateurs. Je te parle d'affecter une assignation à une autre variable.

Pas dans la même cour ? Avec tout ce qu'on peut trouver sur npm ? JE suis sûr qu'il y a autant de bordel que dans pip. Pour à peu près tout et n'importe quoi il y a toujours un package qui existe déjà. Bon c'est quand même très différent, ça dépend beaucoup de ce qu'on veut faire en fait. Mais il y a autant à fouiller je pense.

QuentinC

Pourquoi installer des paquets externes farfelus quand le besoin peut-être comblé par la bibliothèque standard du langage ? C'est bien pour ça que je dis que JS ne joue pas dans la même cour, il n'y a pas de telle bibliothèque.

Alors déjà l'operateur ternaire existe, aussi laid soit-il : a if a > b else b.

Mais surtout si ta fonction fait plus d'une instruction et qu'elle n'est pas déjà dans la lib standard (jette un oeil au module operator pour ton exemple), c'est qu'elle mérite au moins un nom qui justifie son existence et son fonctionnement. On peut ne pas être d'accord avec ça, mais c'est vraiment un choix délibéré.

Si… si on enlève les parenthèses ça marche très bien.

Si ça marche sans parenthèses mais pas avec cela met en évidence le fait que a = b soit une instruction (je binde l'objet référence par b au symbole a dans le namespace local), et non une expression. Cette instruction peut être chaînée mais ne retourne pas de valeur.

Edit : grilled à 150 % par entwanne.

+0 -0

L'opérateur conditionnel ternaire existe en Python, et il se s'agit pas de tricher : a if a > b else b.

Merci, j'ignorais qu'on avait la possibilité d'écrire ça. La concision et la lisibilité de python continueront toujours de m'étonner.

Sans oublier, dans ton exemple, que l'indentation deviendrait alors affreuse.

Là on est d'accord. Imbriquer sans fin des callback dans des callback, c'est d'ailleurs un reproche qu'on fait souvent à JS par exemple.

Non. Tu parles là d'une fonctionnalité du langage qui est de pouvoir chaîner les opérateurs. Je te parle d'affecter une assignation à une autre variable.

JE n'ai pas compris. Pour moi dans ce cas-là c'est pareil.

Pour moi en fait la raison pourquoi la variante avec parenthèses ne fonctionne pas et pourquoi il n'y a pas intérêt qu'elle fonctionne si on veut éviter toute confusion, c'est uniquement parce qu'on peut passer des paramètres nommés à une fonction. Fonctionnalité que je trouve d'ailleurs particulièrement bienvenue dans bien des cas au poins de regretter sa non-existance (ou en tout cas pas de façon si directe) en Java, JavaScript et PHP.

Pourquoi installer des paquets externes farfelus quand le besoin peut-être comblé par la bibliothèque standard du langage ? C'est bien pour ça que je dis que JS ne joue pas dans la même cour, il n'y a pas de telle bibliothèque.

En fait le problème, je crois que c'est la philosophie qui n'est pas la même. En python, on a envie de te filer un énorme pack qui contient déjà plein de trucs très variés dedans. De l'autre côté tu as l'extrême inverse avec lua, qui est livré avec vraiment le strict minimum vraiment strict de chez strict au point que sans lib additionnelle tu ne fais rien (ou presque).

Avec la bibliothèque standard fournie par Node il y a déjà de quoi s'amuser dans le secteur prog système: fichiers, réseau, cryptographie, zlib, web et HTTP, spawn un autre process, … en fait, c'est juste . . . ultra bas niveau par rapport au pack all inclusive tout beau tout joli de python.

Donc sur ce point, mieux vaut arrêter là, on ne pourra jamais être d'accord puisque la philosophie n'est, je pense, en fait, pas la même, et incomparable.

+0 -0

JE n'ai pas compris. Pour moi dans ce cas-là c'est pareil.

Pour moi en fait la raison pourquoi la variante avec parenthèses ne fonctionne pas et pourquoi il n'y a pas intérêt qu'elle fonctionne si on veut éviter toute confusion, c'est uniquement parce qu'on peut passer des paramètres nommés à une fonction. Fonctionnalité que je trouve d'ailleurs particulièrement bienvenue dans bien des cas au poins de regretter sa non-existance (ou en tout cas pas de façon si directe) en Java, JavaScript et PHP.

QuentinC

Tu peux assigner n'importe quelle expression à une variable. Une fonction est une expression (donc les fonctions sont des objets de première classe), un appel de fonction en est une autre.

En revanche, un bloc de définition de fonction n'est pas une expression (il ne vaut rien), tout comme une assignation. De la même manière, tu ne peux pas avoir d'assignation dans une lambda, qui n'accepte qu'une expression (lambda: a = 0).

ultra bas niveau par rapport au pack all inclusive tout beau tout joli de python

Euh, non ? Python te permet de realiser des appels système directement, de spawner des threads et des processus fils, de manipuler directement des flux au travers de leur file descriptor… Pour être plus bas niveau que ça il faudrait que JS franchisse la frontière et permette de développer des modules kernel.

Ce dont je doute.

+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