PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Des bouts de Python cachés   Recently updated !

jeudi 11 décembre 2014 à 10:23

J’aime bien fouiner pour trouver des petits trucs insolites dans mes techos. Cet excellent post sur SO est un bon début, mais on découvre toujours de nouvelles choses.

ChainMap

Vous vous souvenez de ChainMap ? C’est top, mais uniquement en Python 3.

Et bien il existe une version cachée en Python 2 :

from ConfigParser import _Chainmap as ChainMap

C’est pas aussi complet (il n’y a pas les méthodes new_child et parents), mais ça peut servir.

Si néanmoins, vous avez besoin de l’implémentation complète, il existe un backport installable avec pip :

pip install Py2ChainMap

str.startswith, str.endswith et un tuple

Pour beaucoup de situations, une regex en Python, c’est overkill. On a in pour vérifier qu’une chaîne en contient une autre. Et on a str.startswith() et str.endswith() pour vérifier qu’une chaîne commence ou finit par une autre. Avec des strip()/split()/lower(), on finit souvent par s’en sortir, et l’usage de toutes ces méthodes est en général plus performant que d’utiliser re.

Mais si vous avez plusieurs chaînes à vérifier ? Pas de problème, str.startswith() et str.endswith() acceptent aussi des tuples :

>>> "Je suis une chaîne qui a du caractère".startswith('Tu')
False
>>> "Je suis une chaîne qui a du caractère".startswith('Je')
True
>>> "Je suis une chaîne qui a du caractère".startswith(('Tu', 'Je'))
True
>>>

Les espaces entre une instance et son attribut sont ignorés

Bien que la deuxième ligne soit très moche, elle est parfaitement valide :

>>> "Yolo".upper()
'YOLO'
>>> "Yolo"           .upper()
'YOLO'

Or on peut intercepter l’accès aux attributs à la volée en Python grâce aux méthodes magiques.

Ça veut dire qu’on peut implémenter des “symboles” en Python.

class SymbolMaker():
 
    symbols = set()
 
    def __getattr__(self, value):
        symbols.add(value)
 
>>> make_symbol = SymbolMaker()
>>> make_symbol   .start
>>> make_symbol   .end
>>> SymbolMaker.symbols
{'end', 'start'}

Je n’ai pas dit qu’on devrait le faire, j’ai dit qu’on pouvait :) D’ailleurs, y en a qui l’utilisent pour implémenter GOTO en Python. The troll is strong with this “feature”.

Quelle est la différence entre “bloquer” et “en cours d’exécution” ?   Recently updated !

mardi 9 décembre 2014 à 17:57

On vous dit qu’il faut faire attention en utilisant des technologies non bloquantes, car si on bloque dans la boucle d’événement, on bloque tout le programme, et on perd l’intérêt de l’outil.

C’est vrai, mais que veut dire “bloquer” ?

Car si je fais :

for x in range(1000000):
    print(x)

Mon programme va tourner longtemps, et la boucle d’événement va bloquer, n’est-ce pas ?

En fait, “bloquer” est un abus de langage car il y a plusieurs raisons pour bloquer. Dans notre contexte, il faudrait dire “bloquer en attente d’une entrée ou d’une sortie”. D’où l’appellation “Aynschronous non blocking I/O” des technos types NodeJS, Twisted, Tornado, Gevent, etc.

En effet, il faut distinguer deux causes d’attente à votre programme :

Le premier cas est impossible à éviter. Tout au mieux pouvons-nous répartir la charge du programme sur plusieurs cœurs, processeurs voire machines. Le code devra toujours attendre qu’il se termine, mais ça ira plus vite.

Dans le contexte de la programmation non bloquante telle qu’on vous en a parlé, on est donc dans le deuxième cas.

Il ne s’agit alors pas de s’interdire de faire des boucles ou autre opération longue (ou plutôt, c’est un problème d’optimisation ordinaire qui n’a rien à voir avec le fait de bloquer), il s’agit de ne pas “attendre à ne rien faire” quand une opération extérieure est en cours.

C’est ce que font naturellement NodeJS, Twisted, Tornado, Gevent & Co. Quand on fait un échange HTTP, le bout de données part, puis le reste du code continue de tourner, passant à la tâche suivante, en attendant que le paquet traverse le réseau, atteigne l’autre machine, qui vous répond finalement. C’est ce temps, incompressible, sans contrôle de votre côté, durant lequel il ne faut pas bloquer. Le gain de perf est que votre programme ne se la touche pas pendant les temps d’attente, mais bien entendu que VOTRE, lui, code va prendre du temps et “bloquer” le processeur. Il faut bien qu’il s’exécute.

Ce qu’on entend donc par “il ne faut pas faire d’opération bloquante dans un code qui est déjà non bloquant” c’est “il ne faut pas utiliser un outil à l’API bloquante au milieu d’autres outils non bloquants”.

Par exemple, n’utilisez pas requests avec Twisted, car requests est codé pour attendre sans rien faire jusqu’à obtenir une réponse à chaque requête, bloquant Twisted. Utilisez plutôt treq. C’est pareil pour la lecture d’un fichier, une requête de base de données, etc. Et il existe des boucles d’événements ailleurs que sur le serveur : une page Web possède sa propre boucle (c’est pour cela que tout JS est asynchrone), un toolkit GUI comme QT ou GTK aussi (c’est pour ça qu’ils utilisent la programmation événementielle), etc.

Maintenant vous allez me dire : mais pourquoi bloquer alors ? Pourquoi ne pas toujours éviter de bloquer ?

Et bien parce que si on ne bloque pas, on ne peut pas écrire un programme ligne à ligne. On est obligé d’adopter un style de programmation asynchrone puisqu’on ne sait pas quand le résultat de certaines lignes va arriver. Ça veut dire des callbacks, ou des futures, ou des coroutines, ou du message passing… Bref, un truc plus compliqué. Or, on n’a pas forcément besoin de ce niveau de performance. En fait, la grande majorité des programmes n’ont pas besoin de ce niveau de performance. Donc, on bloque en attendant, non pas Godot, mais l’I/O, parce que c’est plus simple à écrire. Pour pas se faire chier.

Il y a bien des moyens de contourner ce problème : les threads, le multiprocessing, les coroutines, etc. Parfois même, on ignore le problème : bloquer quelques ms au milieu d’une boucle d’événements une fois par seconde n’est pas un drame. Une fois que j’ai fini le dossier sur les tests unitaires, je vous ferai un dossier sur la programmation non bloquante, avec aussi une esquisse de la parallélisation.

En attendant, ne stressez pas parce que votre code “bloque” parce qu’il travaille longtemps, assurez-vous juste que les APIs que vous utilisez ne bloquent pas pendant l’I/O, et vous êtes ok.

Et comment savoir ? Et bien si une donnée rentre ou sort de votre programme (ça ne fait pas partie du code source), c’est de l’I/O. Si votre code ressemble à ça :

res = faire_operation_sur_IO()
faire_un_truc_avec_le_res(res)

Alors votre outil est bloquant, puisque qu’il compte sur le fait que la deuxième ligne sera exécutée à coup sûr quand la première sera terminée. Un outil non bloquant exigera quelque chose pour gérer le retour du résultat plus tard: un callback, une promesse, un yield

0bin.net est de nouveau en ligne   Recently updated !

lundi 8 décembre 2014 à 22:02

0bin était down, on a supprimé la page en cause, et on l’a remis up.

On ne sait pas trop comment lutter contre ça. Même TPB a dû avoir 40 noms de domaine pour s’en sortir, et franchement on n’a pas envie de se taper autant de boulot pour un projet qui ne nous rapporte rien.

Que faire donc ?

D’abord, créer une admin pour 0bin pour supprimer une page plus facilement. Ça nous évitera de dépasser les délais la prochaine fois, car je n’étais pas dispo et Max ne savait pas comment faire. C’est pas que c’est compliqué, mais ça prend du temps, faut lire la doc, se connecter au serveur, etc. Et forcément, c’est du temps qu’on préfère passer sur Dota ou des projets qui rapportent des sous.

Ensuite, prendre le temps de vous rappeler que 0bin.net n’est qu’une instance de 0bin. Un exemple. Multiplier les instances est encore le meilleur moyen d’avoir l’outil à disposition. Et le process est plutôt bien documenté.

Mais une fois qu’on a posté son truc sur une instance, si elle tombe, que fait-on ?

Pour le moment on est baisé.

On pourrait donc imaginer de rajouter une fonction de distribution à 0bin. L’idée serait de se lier via une API simple à d’autres 0bin en qui on a confiance. Si quelqu’un poste sur l’un, tous les autres reçoivent une copie. Ainsi dans le lot, il y en aura bien un qui résistera.

Je me tâte à faire ça dans la semaine. Déjà 0bin a besoin d’un portage qui supporte Python 3. Ensuite quelques tests unitaires ne feraient pas de mal. Une fois que c’est fait, je freeze cette version, et je lance 1bin.net, la version suivante. Avec du crossbar.io, du angularjs et du mode distribué. Le truc sera plus lourd, uniquement compatible 2.7 et plus compliqué, donc je préfère en faire une version à part.

Ça se tente, voir si j’arrive à débloquer un peu de temps pour le faire, mais c’est pas impossible. Stay tuned.

Un gros guide bien gras sur les tests unitaires en Python, partie 4   Recently updated !

samedi 6 décembre 2014 à 21:34

Python est un langage très pro, et il y a beaucoup, beaucoup d’outils pour faire des tests.

Après avoir vu pytest, un outil typiquement pythonique sont les doctests, des tests unitaires intégrés dans les docstrings.

Pour rappel, les docstrings, ce sont ces chaînes de caractères qu’on retrouve au début des modules, sous la signature des fonctions ou dans la définition des classes. Elles servent à la documentation de ceux-ci, ainsi on peut la lire dans le code, et dans les vraies docs car les outils standards savent les extraire.

Ça ressemble à ça :

def une_fonction():
    """ Ceci est une docstring.
 
        On peut la lire dans le code source, avec help() dans le shell ou
        dans les docs générés par pydoc et sphinx.
    """
    pass

Et bien ces docstrings, on peut mettre des tests unitaires dedans formatés comme des sessions de shell Python. Cela permet de donner des exemples d’usage, tout en testant son code. C’est chouette.

Musique ?

Musique.

Hello doctests

Faire des doctests n’est pas bien compliqué car c’est du copier coller. On fait une session shell avec ce qu’on veut tester, et on copie-colle le tout dans la docstring. Fastoche.

# on copie juste la session de shell tel quel
def ajouter(a, b):
    """
        >>> ajouter(1, 2)
        3
    """
    return a + b
 
# et on demande à Python de parser les doctests. Directement dans votre fichier
# de code. Si, si. Pas de fichier de tests à part.
if __name__ == "__main__":
    import doctest
    doctest.testmod()

On lance ensuite directement notre fichier de code :

python mon_module.py

Et ça n’affiche absolument rien. C’est parce qu’il n’y a pas d’erreur. On peut avoir le topo en demandant un peu de verbosité avec -v :

python mon_module.py -v
Trying:
    ajouter(1, 2)
Expecting:
    3
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.ajouter
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Les doctests marchent purement en se basant sur le formatage texte. Python prendre la ligne avec >>>, l’exécutera, si la ligne suivante ne contient pas de >>>, il va comparer le résultat de l’exécution de la ligne précédente avec le contenu de la ligne qui la suit.

Ceci passe :

"""
    >>> ajouter(1, 2)
    3
"""

Mais ceci échoue :

"""
    >>> ajouter(1, 2)
    4
"""

Car le résultat AFFICHÉ dans le shell est 3, et non 4.

En cas d’échec, Python vous en dit un peu plus :

python mon_module.py
**********************************************************************
File "mon_module.py", line 6, in __main__.ajouter
Failed example:
    ajouter(1, 2)
Expected:
    4
Got:
    3
**********************************************************************
1 items had failures:
   1 of   1 in __main__.ajouter

Formater ses doctests

Les doctests sont faits pour s’intégrer de manière transparente aux docstrings. On peut donc en mettre autant qu’on veut, au milieu du texte ordinaire de la docstring. Python se base sur les chevrons (>>>) pour savoir quand commence un test, et le saut de ligne pour savoir quand ça se termine. Au delà de ça, le style est libre.

def ajouter(a, b):
    """ Je peux mettre n'importe quoi ici.
 
        Et ici aussi.
 
        Puis intégrer des tests:
 
        >>> ajouter(1, 2)
        3
        >>> ajouter(2, 2)
        4
 
        Et un saut de ligne indique que les tests sont terminés. Mais je peux
        encore en ajouter après si je veux.
 
        >>> ajouter(0, 0)
        0
 
    """
    return a + b

Néanmoins, l’intérêt des doctests est de documenter son code à travers les tests, et donc on adoptera généralement un format tel que ace;:

def ajouter(a, b):
    """ Additionne deux elements.
 
        Exemple :
 
            >>> # on peut mettre des commentaires ici
            >>> ajouter(1, 2) # ou là
            3
            >>> ajouter(2., 2) # fonctionne sur tous les types de nombre
            4.0
 
        La fonction fonctionne en duck typing, et accepte donc tout objet
        qui possède la méthode magique __add__ :
 
            >>> ajouter('a', 'b')
            'ab'
            >>> ajouter([1], [2])
            [1, 2]
    """
    return a + b

Notez comme il est agréable de lire cette docstring : on comprend tout de suite comment utiliser la fonction. En prime, ce sont des tests unitaires qui garantissent que notre fonction va continuer de fonctionner correctement et nous oblige à garder cette doc à jour.

On peut faire des imports dedans ou utiliser temporairement pdb pour debugger. N’importe quel code de shell est valide mais faites attention à ne pas démarrer des boucles infinies comme les event loops des GUI ou lib async.

Voici ce que donnerait l’exemple des articles précédents avec des docstests :

def get(data, index, default=None):
    """ Implémente l'équivalent de dict.get() pour les indexables.
 
        Example :
 
            >>> simple_comme_bonjour = ('pomme', 'banane')
            >>> get(simple_comme_bonjour, 0)
            'pomme'
            >>> get(simple_comme_bonjour, 1000, 'Je laisse la main')
            'Je laisse la main'
    """
    try:
        return data[index]
    except IndexError:
        return default

Problèmes et solutions

Les doctests ne sont pas la Panacée, particulièrement parce que le test se fera sur le résultat AFFICHÉ dans le shell. Cela peut facilement amener à des erreurs.

Déjà, il faut faire attention à la représentation des objets dans le shell Python. La représentation n’est pas forcément la valeur de saisie :

>>> 1.
1.0
>>> "1"
'1'
>>> {"foo": "bar", "une apostrophe : '": "est échapée ainsi qu'un accent"}
{"une apostrophe : '": "est \xc3\xa9chap\xc3\xa9e ainsi qu'un accent", 'foo': 'bar'}

La solution à ce problème est de tester dans le shell les valeurs de retour, et non de le faire de tête. Faites bien gaffe aux espaces qui sont donc significatifs, surtout ceux en fin de ligne. Mon éditeur est configuré pour les virer par défaut, et ça m’a niqué en écrivant l’article :)

Ensuite, il y a des cas où la représentation ne sera pas la même d’un appel à l’autre.

C’est le cas avec les dictionnaires, puisque l’ordre des éléments n’est pas garanti par nature. Ne faites donc pas :

>>> retourne_un_dico()
{'ordre': 'non garanti', 'le': 'resultat'}

Mais plutôt quelque chose qui vous garantit l’affichage :

"""
>>> res = list(retourne_un_dico().items())
>>> res.sort()
[('le', 'resultat'), ('ordre', 'non garanti')]
>>> # ou
>>> retourne_un_dico() == {'ordre': 'non garanti', 'le': 'resultat'}
True
"""

Parfois, on ne peut juste pas garantir l’affichage. Par exemple avec des nombres non prévisibles comme les hash ou les id des objets :

"""
>>> class Test(): pass
>>> repr(Test())
''
"""

7f4687d30fc8 n’est ici pas prévisible. Python met certains cas spéciaux comme celui-ci des flags activables via le commentaire # doctest: +NOM_DU_FLAG.

Par exemple, le flag ELLIPSIS permet de placer ... dans le résultat en guise de joker :

"""
>>> repr(Test()) # doctest: +ELLIPSIS
''
"""

D’autres problèmes similaires peuvent être résolus ainsi. Le flag SKIP permet de sauter un test que vous voulez mettre là, en exemple, mais qui ne doit pas être testé :

"""
>>> # ce test va être ignoré
>>> repr(Test()) # doctest: +SKIP
''
"""

NORMALIZE_WHITESPACE permet de considérer toute séquence de caractères non imprimables comme un espace. 8 tabs ou 4 espaces dans le résultat seront tous considérés comme un espace.

"""
>>> 'ceci est une assez longue ligne divisible' # doctest: +NORMALIZE_WHITESPACE
'ceci    est     une assez longue    ligne divisible'
"""

Les flags sont cumulables, si on les sépare par des virgules dans le commentaire.

Autre astuce, si votre sortie doit contenir un saut de ligne, Python va l’interpréter comme la fin des tests. On peut pallier cela en utilisant <BLANKLINE> :

"""
&gt;&gt;&gt; print('Un saut de ligne\\n')
Un saut de ligne
 
"""

Faites attention aux antislash et autres caractères spéciaux dans vos docstests puisque toute string est parsée deux fois : une fois à l’écriture de la docstring, puis une fois à son exécution. Ici vous voyez que je suis tenu d’échapper mon \n On peut d’ailleurs utiliser les préfixes r (cf: les raw strings) et u sur les docstrings, si un jour vous êtes bloqué par trop d’échappements ou des caractères non ASCII en pagaille, pensez-y.

Un cas particulier est celui des exceptions. LOL, n’est-il pas ?

Pour y répondre, Python décide qu’une expression est levée si il voit Traceback (most recent call last):. Il ignore ensuite tout le texte – qui est donc optionnel et que vous pouvez omettre – jusqu’à ce qu’il rencontre le nom de l’exception levée. À partir de là, il vérifie que le test passe.

Par exemple, si votre exception génère ce traceback :

Traceback (most recent call last):
  File "", line 1, in 
  File "test.py", line 41, in ajouter
    1 / 0
ZeroDivisionError: integer division or modulo by zero

Vous pouvez faire dans votre doctest :

"""
&gt;&gt;&gt; je_leve_une_exception()
Traceback (most recent call last):
ZeroDivisionError: integer division or modulo by zero
"""

Seule la dernière ligne est comparée.

Il est également possible de mettre les doctests dans un fichier texte à part, mais je ne vous le recommande pas. Cela retire l’intérêt principal des doctests : avoir du code exécutable dans la doc. Si on doit avoir un fichier séparé, autant utiliser des tests normaux, bien plus pratiques et complets.

Car il n’y a pas de tear down, setup ou fixtures avec les docstests. Ca reste un outil basique.

Sachez néanmoins que les doctests sont parfaitement compris par pytest, il suffit juste de lui demander de les exécuter avec l’option suivante :

py.test --doctest-modules

Dans ce cas, il n’est pas nécessaire de faire à la fin de chaque fichier contenant des doctests :

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Quand utiliser les doctests ?

Généralement, on utilise un mélange des tests ordinaires (dans notre cas des tests pytest plutôt que unittest) et des doctests.

On utilisera des doctests pour les objets ou les fonctions simples et indépendantes. J’entends par là, des fonctions et des objets qui prennent uniquement des types basiques en paramètres, et qui retournent uniquement ces types basiques en paramètres. Pour les objets, ils doivent avoir peu de méthodes.

Pour tout le reste, on utilisera des tests ordinaires.

Par exemple, si vous avez une fonction comme notre exemple get(), les doctests sont un bon choix. En revanche, si vous avez un objet Server qui est un serveur HTTP, ou une fonction qui prend un objet Server en paramètre, il vaut mieux utiliser les tests ordinaires.

Il est tout à fait possible, et même agréable, de mettre quelques tests simples en doctests qui aident à la documentation, et de faire les tests les plus compliqués via pytest.

Prochaine étape, les mocks. Parti de là, je pourrai vous dire quelles parties de votre programme tester en priorité, et comment. Au début je voulais faire l’inverse, mais finalement, c’est plus pratique.


Télécharger le code de l’article

Dans quels secteurs retrouve-t-on Python ces temps-ci ?   Recently updated !

vendredi 5 décembre 2014 à 21:31

J’utilise le compte Tweeter du blog (n’oubliez pas qu’il y a un flux RSS des tweets) pour RT des offres d’emploi en rapport avec Python régulièrement. Des stages, des demandes de freelance, des embauches, etc. J’essaye de rester dans des offres francophones ou dans des pays limitrophes à la France, histoire que ce soit des infos utiles.

Ce faisant, j’ai pu noter au fil de l’année les secteurs qui sont le plus présents parmi ces recrutements, en tout cas via micro-blogging.

En premier, on a le Web, évidemment. Essentiellement du Django. Rien d’étonnant, ce n’est pas lié à Python en particulier, c’est juste le secteur informatique qui est le plus actif, et tout langage qui peut faire du HTTP correctement amène forcément du taf. Les outils pour cela en Python sont de bonne qualité, et on le retrouve donc un peu partout.

Ensuite vient la finance.

Ceci me gène un peu. Je ne les retweete pas en général, car éthiquement parlant, je ne suis pas à l’aise avec les institutions financières actuelles, mais ce n’est pas le sujet de l’article. Néanmoins ce sont d’excellentes opportunités techniques : analyse de données, temps réel, traitements logiques, UI, grosses infras, des moyens et un travail avec un impact fort IRL. Que Python s’impose dans ces milieux n’est pas une surprise : ils privilégient pragmatisme et efficacité. Or Python est un langage clair, qui permet une itération rapide en dev, des traitements complexes tout en restant facile à maintenir.

En 3ème position (selon les stats de l’INDMS, bien sûr), on retrouve la science : les chercheurs, biologistes, physiciens, géographes, astronomes et tous corps de métiers qui ont besoin de pouvoir adapter facilement des algos, transcrire des maths en code, traiter des gros jeux de données… Python est un langage fantastique pour cela, et sont apparues tout un tas de libs, soit pour gagner en perf, soit pour coller aux règles du métier. On retrouve du coup le langage comme solution de scripting dans des logiciels pros comme les SIG, des bases de données moléculaires et autres gros morceaux. C’est un usage tellement important que la prochaine version va embarquer un nouvel opérateur mathématique pour multiplier les matrices : le @.

Et puis il y a l’admin système et l’automatisation. Python remplace Perl un peu partout, dès qu’un script bash ne suffit plus. Car Python est simplement plus lisible et maintenable que ces deux langages, et il est aussi maintenant plus versatile, grâce au travail de la communauté. Des initiatives comme OpenStack ont fait de Python un acteur majeur dans les solutions de déploiement, de cloud, d’archi distribuées. C’est néanmoins très spécialisé, et concurrentiel, et donc trouver un bon dev là dedans ne doit pas être facile.

Notre pénultième, c’est la gestion d’entreprise. Principalement à travers le produit Odoo (aka OpenERP) qui s’occupe de tout, du personnel aux clients en passant par la facturation. C’est un peu usine à gaz, mais ça vaut toujours mieux que SAP. Ceci dit, en dehors de cela, Python est un fantastique langage glu, du fait qu’il est capable de lire et générer de nombreux formats (MS Office, LibreOffice, PDF, images) et base de données (MySQL, PostGres, Oracle, Access, et d’autres plus obscures). On peut même parler certains protocoles de machines outils. Dans des usines qui ont 15 ans d’empilement technologique et de migrations avec des morceaux legacy qui traînent partout, ça change la vie. Surtout qu’on peut maintenant remplacer VB avec Python presque partout, y compris dans Excel.

En dernier, il y a le Big Data. Derrière ce mot pompeux se cache les analyses de données massives, l’intelligence artificielle et en fait, tout ce qui concerne extraire du sens à partir d’un gros blob qui n’appartient pas aux métiers cités précédemment. Interprétation du langage humain, études d’images ou de son, réseaux sémantiques, généralement sous forme de grandes collections d’échantillons. Là c’est du haut niveau, mais ça fait justement des postes intéressants.

Malgré Kivy, le Rasberry Py et Micro Python, je ne vois pas encore le langage décoller dans l’embarqué, sur téléphone, sur l’internet des objets… WAMP va peut être changer la donne, mais pas tant que qu’on aura pas créé une API plus sexy.

Les jeux vidéos sont aussi un secteur qui manque de Python, et c’est dommage. On lui préfère souvent le Lua, à part dans quelques cas comme Civilisation ou Eve Online.

Apparemment Python paie bien, mais honnêtement j’en ferai pas un critère de choix : ces choses là changent. Si on veut de la thune, on fait du Cobol, emploi bien payé garanti.