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.