PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Go to (in asyncio) considered harmful 26

jeudi 7 juin 2018 à 09:31

Dijkstra était un intellectuel pédant, mais quand il a écrit cette lettre célèbre, il a comme souvent mis le doigt sur un truc fondamental. Et quand l’auteur de Trio, une stack toute neuve concurrente d’asyncio, lui a fait écho 50 ans plus tard, ça a beaucoup discuté sur les mailing lists et les bugs trackers.

Nathaniel J. Smith, le dev susnommé, en a profité pour introduire une nouvelle primitive, actuellement surnommée la nursery, pour répondre au problème. Une idée visiblement tellement bonne que notre Yury préféré a décidé de la porter à asyncio. La boucle d’événements est bouclée, si je puis dire.

Mais une autre chose intéressante en découle : on a mis en lumière la présence d’un goto dans asyncio, et qu’il y a de bonnes pratiques, validées par Guido himself, pour coder avec cette lib pour éviter les douleurs.

What the fuck are you talking about ?

Le problème du goto, c’est que l’instruction permet d’aller de n’importe où à n’importe où. Cela rend le flux du programme très dur à suivre. Pour éviter cela, on a catégorisé les usages clean du goto: répéter une action, changer de comportement en fonction d’un test, sortir d’un algo en cas de problème, etc. Et on en a fait des primitives : les if, les while, les exceptions… Dans les langages les plus modernes, on a carrément viré le goto pour éviter les abus et erreurs. Joie.

Dans asyncio, le “goto” en question se trouve quand on veut lancer des tâches en arrière plan, comme ceci :

import asyncio as aio
loop = aio.get_event_loop()
aio.ensure_future(foo())  # GOTO !
aio.ensure_future(bar())  # GOTO !
loop.run_forever()

Le problème d’ensure_future() est multiple:

En prime run_forever() est un piège à con, car les exceptions qui arrivent dans la boucle sont logguées, mais ne font pas crasher le programme, ce qui rend le debuggage super rude, même avec debug mode activé (dont de toute façon personne ne soupçonne l’existence).

La solution asyncio

import asyncio as aio
loop = aio.get_event_loop()
loop.run_until_complete(aio.gather(foo(), bar())

En plus d’être plus court, les exceptions vont faire planter le programme, la loop s’arrêtera quand les coroutines auront fini leur taff, leur flux a un début et une fin encapsulés par le gather(). Ceci est encore plus visible si on met le même code à l’intérieur d’une coroutine à l’intérieur d’une coroutine à l’intérieur d’une coroutine plutôt qu’à la racine du programme. En effet dans un exemple si simple, on se borne au démarrage et à l’arrêt de la boucle. Mais je suis paresseux.

Donc, c’est la bonne pratique, mais tout le monde ne le sait pas.

Pardon, correction.

Tous les devs Python ne connaissent pas asyncio. Parmi ceux qui connaissent asyncio, une petite partie comprend comme ça marche.

Dans ce lot rikiki, un pouillième sait que c’est la bonne pratique.

En fait, gather() est probablement la fonction la plus importante d’asyncio, et pourtant elle apparaît à peine dans la doc. C’est la malédiction d’asyncio, une lib que tout le monde attendait pour propulser Python dans la league des langages avec frameworks modernes, mais qui commence à peine à devenir utilisable par le commun des mortel en 2018. Et encore.

Il ne faut jamais utiliser ensure_future() à moins de vouloir attacher un callback à la main dessus, ce qui n’est probablement jamais ce que vous voulez à cette époque merveilleuse ou existe async/await. ensure_future() est un goto, gather() est un concept de plus haut niveau.

Mais deux problèmes demeurent…

Contrairement au goto banni de Python, ensure_future() est là, et va rester. Donc n’importe quel connard peut dans un code ailleurs vous niquer profond, et en tâche de fond.

ensure_future() (ou son petit frère EventLoop.create_task()) reste le seul moyen valable pour lancer une tâche, faire quelque chose, lancer une autre tâche, puis enfin faire un gather() sur les deux tâches:

async def grrr():
    task1 = aio.ensure_future(foo())
    # faire un truc pendant que task1 tourne
    task2 = aio.ensure_future(bar())
    # faire un truc pendant que task1 et task2 tournent
    # On s'assure que tout se rejoint à la fin:
    await aio.gather(task1, task2)

Et puis, faire une pyramide de gather() dans tout son code pour s’assurer que tout va bien de haut en bas, c’est facile à rater.

La nursery : la solution de trio

Une nursery agit comme un scope qui pose les limites du cycle de vie des tâches qui lui sont attachées. C’est un gather(), sous stéroide, et avec une portée visuellement claire:

async def grrr():
    async with trio.open_nursery() as nursery:
        task1 = nursery.start_soon(foo)
        # faire un truc pendant que task1 tourne
        task2 = nursery.start_soon(bar)
        # faire un truc pendant que task1 et task2 tournent

Les taches sont garanties, à la sortie du with, de se terminer. Le ensure_future() n’a pas d’équivalent en trio, et donc aucun moyen de lancer un truc dans le vent sans explicitement lui passer au moins une nursery à laquelle on souhaite l’attacher.

Résultat, on ne peut plus faire de goto, et le flux du program est clair et explicite.

Notez que, tout comme if et while ne permettaient rien qu’un utilisateur soigneux de goto ne pouvait faire, la nursery ne permet rien qu’un utilisateur soigneux de ensure_future() ne peut faire. Mais ça force un ensemble de bonnes pratiques.

Évidemment, on peut ouvrir une nursery dans un bloc d’une autre nursery, ce qui permet d’imbriquer différentes portées, comme on le ferait avec un begin() de transaction de base de données. Or, une exception à l’intérieur d’une nursery bubble naturellement comme toute exception Python, et stoppe toutes les tâches de la nursery encore en train de tourner. Alors qu’avec asyncio vous l’avez dans le cul.

En définitive, c’était la pièce manquante. La moitié du boulot avait était faite quand on a introduit un moyen de gérer des tâches asynchrones qui dépendent les unes des autres, en remplaçant les callbacks par un truc de haut niveau : async/await. Il restait la gestion des tâches en parallèle qui se faisait encore selon les goûts et compétences de chacun, mais la nursery va remplir ce vide.

Cela devrait être intégré à asyncio en Python 3.8, soit une bonne année et demie pour ceux qui ont la chance de pouvoir faire du bleeding edge.

Comme certains ne voudront pas attendre, je vous ai fait un POC qui vous montre comment ça pourrait marcher. Mais cette version ne sera jamais utilisée. En effet, elle intercepte ensure_future() (en fait le create_task() sous-jacent) pour attacher son résultat à la nursery en cours, évitant tout effet goto, et ça péterait trop de code existant. Mon pognon est plutôt sur un gros warning émis par Python quand on fait une gotise.

Dernier mot: s’il vous plaît, allez voter pour change le nom de nursery. C’est beaucoup trop long à taper pour un truc qu’on va utiliser tout le temps.

Once you go black, you never go back 19

mercredi 6 juin 2018 à 13:16

L’indentation obligatoire et l’existence du PEP8 sont pour moi deux features fondamentales de Python, limitant énormément la quantité de code illisible qu’on trouve dans la communauté.

Malgré cela, le reformatage de code reste une tache courante, et nécessaire, mais un gâchis énorme de temps. D’abord il faut décider comment on va formater, ce qui en équipe veut dire débat sur le pire sujet qui soit: le goût. Ensuite il faut mettre en place des configurations de linter (flake8, pylint, etc), et potentiellement l’infra qui va avec (tox, hooks git, CI…).

Pour cette raison, de nombreux outils de formatage automatique ont vu le jour. Le premier a été autopep8, et plus tard yapf de Google.

Mais ces deux outils ont quelques soucis:

Le monde du langage Go a choisi une stratégie différente: la technique du “ta gueule”.

Et aussi: ta gueule

Et aussi: ta gueule

Cette technique subtile et raffinée s’est incarnée dans l’outil Gofmt, qui est fourni par défaut avec go, et n’a AUCUN réglage.

Le résultat, tout le monde a fermé sa gueule et a adopté l’outil.

Est-ce que le formatage est parfait ? Non.

Est-ce qu’il plaît à tout le monde ? Absolument pas.

Est-ce qu’il fait fermer sa putain de gueule à tout le monde afin qu’on puisse enfin retourner à des choses plus importantes comme coder ?

Yes !

Gofmt produit un formatage suffisamment clair et pragmatique, et comme il est fortement ancré dans la communauté, tout le monde est à la même enseigne. Passer d’un code à un autre est facile. Pas de temps perdu à discuter du style ou à tweaker ses linters. Tout le monde lance go fmt (aka go ferme ta …) et on passe à autre chose.

Dernièrement facebook a décidé de faire pareil, et à pondu en open source black (en référence à Henry Ford), un outil de formatage en Python, qui n’a que 2 réglages. Il suit le PEP8, mais évidemment sa propre interprétation, et ne propose rien d’autre.

Black a aussi l’avantage de fournir des diffs assez petits, et surtout, vérifie si l’AST change après un reformatage, et annule le cas échéant, garantissant que le sens de votre code n’est pas altéré.

Est-ce que j’aime toutes les règles de formatages de black ? Non.

Est-ce que regarder sa sortie me donne parfois envie de me bouffer les couilles parce que franchement, qui pense que c’est une bonne idée d’aligner les choses comme ça ? Parfois.

Mais c’est good enough.

Et du coup, l’adoption de black a été très rapide dans la communauté, et il a été appliqué à heroku, requests, tablib, envoy, clint, fabric 2 et pytest. 4000 stars sur github.

Installation

Évidemment, ça se pip install, mais uniquement sur Python 3.6. Black peut checker du code 2.7, mais il lui faut du 3.6 minimum pour exister, donc on l’installe en parallèle. Évidemment, on peut l’intégrer à ST, Vim ou VSCode. Si votre projet utilise un Python different, il faut donc dans les options faire pointer l’exécutable vers l’installation séparée.

Résultat

Dans l’esprit du lien partagé par Seb, créons un générateur de titre de film porno:


import random

subject_qualifiers = ( "shy", "mature", "busty", "hot", "horny", "ebony", "quiet", "excited", "naughty", "bad", "cheating", "beautifull", "gorgeous", "drunk", "emo", "fat", "chubby", "goth", "lingery wearing", "latex enthousiast", "placid", "energic", 'slutty', 'sweaty', 'curvy', )

subjects =(
    'teen',
    'doll',
    'brunette',
    'blonde',
    'midget',
    'milf',
    'bitch',
    'babe',
    'sister',
    'step-mom',
    'vixen',
    'secretary',
    'real estate agent',
    'teacher',
    'student',
    'schoolgirl',
    'cheer leader',
    'asian tourist',
    'babysitter',
    'ex girlfriend',
    'nurse',
    'squirtter',
    'model',
    'granny',
    'furry',
)

actions = (
        "recieves anal",
        "get busted",
        "driven to bukakke",
        "taught double penetration",
        "fucked hard",
        'gently chocked',
        'punished',
        'forced into blow job',
        'pounded',
        'creampied',
        'ass raped',
        "eaten",
        "get her pussy wet",
        "shamed",
        "get an orgasm for the first time",
        'lead to loud climax',
        'offered best sex of her life',
        'worn out',
        'cured from boredom',
        'warmed up',
        'loved in and out',
        'generously oiled',
        'shocked and impressed',
        'decieved into giving it',
        'woke up roughly',
        'get sexy massage',
        'ridden to exhaustion',
        'turned into a lavish slave',
        'never submit to torture',
        'rebels against abuses',
        'taken in every possible way',
        'enjoy the 10 inches provided',
)

actors = (
        "pawn shop owner",
    "corrupted cop",
    "dirty plumber",
    "big ass nigga",
    "sport coach"
    "her boss",
    "twisted psychiatrist",
    "ripped doctor",
    "crispy fire fighter",
    "smug playboy",
    "skinny geek",
    "eccentric millionaire",
    "airplane pilot",
    "movie star",
    'football team',
    'her big brother',
    'security guard',
    'hairy beast',
    'wasted guitard player',
    'hung indian immigrant',
    'a guy twice her size',
    '17 guys in a row',
    'her ideal man',
    'her secret prince charming',
    'weirdo albinos',
    'muscle giant',
    'the worst cook ever',
    'cable man',
    'more men that she can count',
    'two friendly brothers',
    'enrike strongsteel'
)

contexts = (
    "on the beach","in a cheap motel","in the back of a van",
    "in airplane toilets", "for hours", "to pay back her depts",
    "for a stupid mistake", "and it gets better", "and ask for more",
    "because she could", "in exchange for a favor",
    "right next to her boyfriend", "as a reward",
    "hopping to get him back", "caught on security cam", "every monday",
    "in a barn", "but that's not all", 'but she has a secret',
    "and she has a dick too", 'before inviting her friend over',
    'while her father is watching', 'with her ', "while auditing for a role",
    "to get her job back", "for an interview", "in exclusive sex tape",
    "again and again", ", begging to stop", "for a change", "for chrismas",
    "in public", 'in a back alley', "during a concert", 'on her death bed'
)

punctuation = ('','!','!!','...')

def get_title(subject_qualifiers, subjects, actions, actors, contexts) :


    qualifier = random.choice(subject_qualifiers)
    subject = random.choice(subjects)
    action = random.choice(actions)
    actor = random.choice(actors)
    context = random.choice(contexts)

    return f"{qualifier} {subject} {action} by {actor} {context}" .capitalize()


if __name__ == "__main__":
    print(get_title(subject_qualifiers = subject_qualifiers, subjects=subjects,
                    actions=actions, actors=actors, contexts=contexts))

Usage:

$ python3.6 porn_title_generator.py
Chubby model loves bukakke by skinny geek during a concert
$ python3.6 porn_title_generator.py
Busty bitch rebel against abuses by security guard but she has a secret
$ python3.6 porn_title_generator.py
Lingery wearing student creampied by weirdo albinos on the beach
$ python3.6 porn_title_generator.py
Horny bitch offered best sex of her life by hairy beast in airplane toilets
$ python3.6 porn_title_generator.py
Emo blonde punished by airplane pilot on the beach
$ python3.6 porn_title_generator.py
Quiet squirtter lead to loud climax by wasted guitard player while auditing for a role
$ python3.6 porn_title_generator.py
Emo babysitter get her pussy wet by football team caught on security cam
$ python3.6 porn_title_generator.py
Busty asian tourist taken in every possible way by muscle giant and she has a dick too
$ python3.6 porn_title_generator.py
Placid milf ass raped by muscle giant in a back alley
Je soupçonne un coup des frères Markov

Je soupçonne un coup des frères Markov

On applique black, zero réglage, usage simplissime:

$ black . # appel recursif, modification in place par défaut

Le résultat.

import random

subject_qualifiers = (
    "shy",
    "mature",
    "busty",
    "hot",
    "horny",
    "ebony",
    "quiet",
    "excited",
    "naughty",
    "bad",
    "cheating",
    "beautifull",
    "gorgeous",
    "drunk",
    "emo",
    "fat",
    "chubby",
    "goth",
    "lingery wearing",
    "latex enthousiast",
    "placid",
    "energic",
    "slutty",
    "sweaty",
    "curvy",
)

subjects = (
    "teen",
    "doll",
    "brunette",
    "blonde",
    "midget",
    "milf",
    "bitch",
    "babe",
    "sister",
    "step-mom",
    "vixen",
    "secretary",
    "real estate agent",
    "teacher",
    "student",
    "schoolgirl",
    "cheer leader",
    "asian tourist",
    "babysitter",
    "ex girlfriend",
    "nurse",
    "squirtter",
    "model",
    "granny",
    "furry",
)

actions = (
    "recieves anal",
    "get busted",
    "driven to bukakke",
    "taught double penetration",
    "fucked hard",
    "gently chocked",
    "punished",
    "forced into blow job",
    "pounded",
    "creampied",
    "ass raped",
    "eaten",
    "get her pussy wet",
    "shamed",
    "get an orgasm for the first time",
    "lead to loud climax",
    "offered best sex of her life",
    "worn out",
    "cured from boredom",
    "warmed up",
    "loved in and out",
    "generously oiled",
    "shocked and impressed",
    "decieved into giving it",
    "woke up roughly",
    "get sexy massage",
    "ridden to exhaustion",
    "turned into a lavish slave",
    "never submit to torture",
    "rebels against abuses",
    "taken in every possible way",
    "enjoy the 10 inches provided",
)

actors = (
    "pawn shop owner",
    "corrupted cop",
    "dirty plumber",
    "big ass nigga",
    "sport coach" "her boss",
    "twisted psychiatrist",
    "ripped doctor",
    "crispy fire fighter",
    "smug playboy",
    "skinny geek",
    "eccentric millionaire",
    "airplane pilot",
    "movie star",
    "football team",
    "her big brother",
    "security guard",
    "hairy beast",
    "wasted guitard player",
    "hung indian immigrant",
    "a guy twice her size",
    "17 guys in a row",
    "her ideal man",
    "her secret prince charming",
    "weirdo albinos",
    "muscle giant",
    "the worst cook ever",
    "cable man",
    "more men that she can count",
    "two friendly brothers",
    "enrike strongsteel",
)

contexts = (
    "on the beach",
    "in a cheap motel",
    "in the back of a van",
    "in airplane toilets",
    "for hours",
    "to pay back her depts",
    "for a stupid mistake",
    "and it gets better",
    "and ask for more",
    "because she could",
    "in exchange for a favor",
    "right next to her boyfriend",
    "as a reward",
    "hopping to get him back",
    "caught on security cam",
    "every monday",
    "in a barn",
    "but that's not all",
    "but she has a secret",
    "and she has a dick too",
    "before inviting her friend over",
    "while her father is watching",
    "with her ",
    "while auditing for a role",
    "to get her job back",
    "for an interview",
    "in exclusive sex tape",
    "again and again",
    ", begging to stop",
    "for a change",
    "for chrismas",
    "in public",
    "in a back alley",
    "during a concert",
    "on her death bed",
)

punctuation = ("", "!", "!!", "...")


def get_title(subject_qualifiers, subjects, actions, actors, contexts):

    qualifier = random.choice(subject_qualifiers)
    subject = random.choice(subjects)
    action = random.choice(actions)
    actor = random.choice(actors)
    context = random.choice(contexts)

    return f"{qualifier} {subject} {action} by {actor} {context}".capitalize()


if __name__ == "__main__":
    print(
        get_title(
            subject_qualifiers=subject_qualifiers,
            subjects=subjects,
            actions=actions,
            actors=actors,
            contexts=contexts,
        )
    )

L’indentation est revue et normalisée vers 4 espaces, les espacements et sauts de ligne sont rééquilibrés (limite de caractères à 88 ), les quotes deviennent toutes ‘”‘. C’est lisible. Le code marche toujours.

Problem solved.

Je fais mon coming out 47

mardi 29 mai 2018 à 13:48

J’utilise VSCode

J’ai vraiment du mal à m’en remettre, et j’ai des proches qui utilisent Vim et ne sont pas toujours encore à l’aise avec l’idée. J’ai refusé d’utiliser Visual Studio à de nombreuses reprises, alors tester son petit frère était déjà un pas osé. Un truc Microsoft. Un truc écrit en Javascript.

Mais bon, j’aime ça, et il faut pas avoir honte de qui on est.

Le fait que ce soit libre et multiplateforme pour un produit Microsoft est surprenant, néanmoins c’est le maintien continu de l’excellent comportement de la team derrière qui est le plus bluffant: respectueux, proche des utilisateurs, sans bullshit…

Le fait que ce soit facile à installer et utiliser pour un projet javascript est surprenant, néanmoins c’est l’excellente performance du produit qui est le plus bluffant: temps démarrage, réactivité du scroll, gestion de gros projets…

Alors j’ai continué à le garder sous le coude, en parallèle à Sublime Text.

Et quelque chose de subtil a changé, chaque jour, sublime dont j’ai pourtant payé la licence, me faisait de moins en moins bander. Je sollicitais de plus en plus VSCode. Jusqu’à ce que ça devienne mon éditeur par défaut.

Oh, ST et moi on se voit toujours. Pour ouvrir un petit fichier vite fait, taper un article, tester un truc.

En revanche dès que c’est un projet, j’ai un éditeur Electron made in Redmond pour ça, et il me rend heureux.

L’ergonomie de la bestiole

Les auteurs de VSCode ont pompé tous les éditeurs les plus populaires, goulûment. Ils ont optimisé le temps de démarrage à mort, et même si on n’a pas la vitesse d’un ST ou d’un Vim, ça reste moult fois plus rapide que la vaste majorité de la concurrence. Pas de splash screen à rallonge et ce moment de doute où on n’est pas sûr d’avoir vraiment cliqué sur le bouton. J’aime bien Jetbrain mais le startup de PyCharm me fout les boules à chaque fois.

Côté apparence, on retrouve des lignes épurées avec peu de boutons, des tabs, la fameuse bird view du code de ST, un Head Up Display, une statut-bar très riche et le “go to anywhere” que tout le monde adore depuis Mate.

La force de VSCode c’est l’expérience de son équipe : ils ont bien compris ce que les utilisateurs faisaient le plus souvent, et l’ont mis à porter de main. Un clic pour faire un split view ou afficher le terminal intégré. Mais pas de fonction “Imprimer”. Une barre latérale donne l’accès à 4 autres modes, un pour la recherche dans tout le projet, un pour git (et rien d’autre), un pour le debuggeur intégré, et un pour installer des extensions.

Une foule de choses sont configurables, avec une interface qui mélange fichiers de config et aide à la saisie. C’est étrange la première fois qu’on met le nez dans “paramètres de l’utilisateur” ou “ouvrir les raccourcis clavier”. Ni vraiment une fenêtre avec des formulaires. Ni vraiment un JSON à éditer à la main. Un peu des deux. Et c’est super bien fait.

Ceci dit, comme les réglages par défaut sont assez sains, un junior n’aura pas à s’en soucier et pourra tout de suite commencer à introduire des bugs dans votre projet.

L’éditeur

Aucune innovation. Aucune tentative de faire différent de la concurrence. C’est du classique, c’est propre, et ça marche. On peut bien entendu choisir entre plusieurs mode de saisie (mode VI, Emarcs, Sublime, etc), mais perso je reste avec le mode original et quelques raccourcis custo.

Derrière, toutes les fonctionnalités modernes sont là: multi-curseur, sélection/recherche incrémentale, snippets (emmet inclus !), complétion des mots les plus utilisés, navigation par symbole, hot exit. L’avantage, c’est que comme VSCode joue la carte de l’interface minimaliste, on n’a pas besoin de connaitre tout ça, et on peut juste commencer à taper, tout en apprenant chaque feature au fur et à mesure de ses progrès. C’est un excellent éditeur pour débutant en ce sens. Mais les powers users qui aiment malgré tout la souris et les onglets y trouveront leur compte.

La coloration syntaxique est irréprochable (heureusement), mais on voit qu’ils ont du faire des concessions. Par exemple au démarrage, seule la partie de votre viewport est colorée. Il faut attendre une à deux secondes sur les gros projets pour que le reste du fichier le soit, histoire de pas freezer tout le bouzin.

Le bon côté de ça c’est que c’est très fluide. Bon évidemment j’ai 8 coeurs et 32Go de RAM. J’ai tenté l’aventure sur une VM avec 2 de rames et un tout petit coeur, et c’est pénible. Au repos avec quelques tabs ouverts, le truc s’engouffre quand même ses 700Mo de mémoire vive. N’oubliez pas que c’est du V8 derrière.

En comparaison ST en bouffe 300, et Vim, heu, LOL.

Intégration Git

L’intelligence de cette feature, c’est qu’ils se sont limités aux opérations simples et courantes. Permettre de naviguer dans l’espace temps ou de lancer son merge --rebase, c’est dur à faire correctement. Donc VSCode n’essaye pas.

Il affiche juste la liste des fichiers qui sont modifiés et/ou en staging, permet de les bouger de l’un à l’autre ou annuler les modifications, et de faire un commit rapidement. Un clic sur un fichier l’ouvre en mode diff avec HEAD. C’est tout.

C’est pas 1% de ce que permet de faire Git.

Mais c’est facilement 69% de mon usage de Git. Du coup c’est super pratique. Combiné avec le terminal intégré, et vous pouvez gérer presque tout le repo sans sortir de l’éditeur.

Le debuggeur

Je ne l’utilise jamais et je préfère ipdb. Pour le moment, en Python, il est trop lent. Les devs JS en disent du bien, vu qu’apparemment il est capable de se connecter directement au navigateur et comprend TypeScript de manière transparente.

La recherche

Rien à dire. C’est rapide. Ça marche. Ça supporte les trucs les plus importants: case insensitive (activé par défaut), regex, in sélection, dans tous les fichiers, filtrés par extension, et tout le bordel. Cliquer sur le résultat ouvre le fichier à la bonne ligne. Pas de modale qui bloque l’UI.

Pas de surprise, donc. Mais pas de mauvaises surprises.

L’indexage est configurable par projet, ce qui est indispensable dès que vous avez quelque chose d’un peu complexe.

La recherche de fichiers par nom est absente puisque ferait doublons avec “Go To Anywhere”.

Intégration des langages

Là, on attaque la partie intéressante. VSCode est neutre dans son traitement des langages, et toutes les features avancées se font donc via des extensions. L’astuce, c’est que l’équipe supporte officiellement certaines extensions, et elles sont donc d’excellente qualité.

Le support de Python est phénoménal. C’est simplement le meilleur après celui de PyCharm (et de pas beaucoup), ce qui n’est pas peu dire, vu que Jetbrain fait probablement des messes noires et des sacrifices à Quetzalcoatl pour obtenir ce résultat.

Python est notoirement difficile à outiller de par son très grand dynamisme.

Mais là, c’est beau.

Pylint est activé par défaut, et flake8 ainsi que mypy sont optionnellement activables. Leurs préréglages sont de bonne qualité, particulièrement celui de mypy qui est normalement inutilisable out of the box. L’éditeur vous prompte pour l’installation quand il détecte qu’ils sont absents, et lance tout ça pour vous.

Tout est configurable par projet, et donc si vous spécifiez un virtualenv pour votre projet (ce qui vous devriez toujours faire), VSCode va détecter que les outils ne sont pas dedans, vous proposer de les installer, et le faire pour vous.

Du coup, bénéficier des types hints, de la détection des erreurs de syntaxes, des variables non déclarées et des imports manquants ou inutiles est beaucoup plus facile que sur n’importe quel compétiteur. Ok, sauf PyCharm. Mais personnellement je l’appelle PyChiderme.

Si VSCode ne supporte pas nativement un outil, il existe probablement une extension pour ça. Par exemple, il y a une extension pour black, qui est à Python ce que Gofmt est à Go, et que j’installe donc maintenant à chaque nouveau projet.

L’intégration de ces outils est excellente :

VScode m’a même surpris à détecter mes tests unitaires, m’a proposé d’installer pytest puis de lancer tout ça.

Cependant, pour vraiment parler de l’intégration de Python dans VSCode, il me faut mentionner IntelliSense. C’est un terme marketing inventé par MS pour caractériser toutes les fonctionnalités autour de la compréhension que l’éditeur à du code, et des opérations qu’il propose dessus.

Ok, ok, c’est un mot 100% bullshit.

Mais bordel, ça marche.

La complétion du code est excellente, et marche sans aucun réglage. Avec la lib standard bien entendu, mais aussi avec votre code, et toutes les libs installées dans votre virtualenv (si vous avez précisé le chemin vers ledit env dans les settings du projet, of course, il est pas devin).

VSCode affiche les docstrings, les params et propose d’aller à la définition de n’importe quoi en un clic.

Et si comme moi vous avez passé un temps fou à essayer d’obtenir le même résultat sous ST/Vim/Whatever en chargeant what mille plugins et en changeant 600 valeurs de configs, vous comprendrez que c’est juste, topissime.

Quelques infos

Les réglages de VSCode de base sont bons. C’est vraiment une partie de ce qui fait la force du projet: moins de bordel à faire soi-même. Mais comme il est bien configurable, il ne faut pas s’en priver. Quelques trucs que je fais toujours:

Installer une police avec des ligatures

Genre Fira-Code.

Et activer les settings:

    "editor.fontFamily": "'Fira Code', 'Droid Sans Mono', 'Courier New', monospace, 'Droid Sans Fallback'",
    "editor.fontLigatures": true,

Exclure plein de fichiers

J’ai pas du tout envie que “Go to anywhere”, la recherche des fichiers ou l’indexage git charge des trucs inutiles. Donc j’ai des settings de base de nazi:

    "files.exclude": {
        "**/.git": true,
        "**/.svn": true,
        "**/.hg": true,
        "**/.DS_Store": true,
        "**/dist": true,
        "**/build": true,
        "**/env/**": true,
        "**/venv/**": true,
        "**/virtualenv/**": true,
        "**/node_modules": true,
        "**/bower_components": true,
        "**/vendors": true,
        "**/__pycache__": true,
        "**/**/*.pyc": true
    },
    "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/node_modules/**": true,
        "**/build/**": true,
        "**/dist/**": true,
        "**/env/**": true,
        "**/venv/**": true,
        "**/virtualenv/**": true,
        "**/bower_components/**": true,
        "**/vendors/**": true,
        "**/__pycache__": true,
        "**/**/*.pyc": true
    },

Mes settings par projet sont généralement encore plus restrictifs.

Je change les params de zoom

 "window.zoomLevel": 2,
 "editor.mouseWheelZoom": true,
 "editor.fontSize": 10,

Je vire la télémétrie

Je suis pas sous Windows 10, merde.

    "telemetry.enableCrashReporter": false,
    "telemetry.enableTelemetry": false,

Je mets des barres verticales

    "editor.rulers": [
        79, # PEP8
        88, # Black
        120 # Javascript
    ],

Ergonomie perso

    "editor.renderWhitespace": "none", # overridé par projet
    "editor.renderIndentGuides": true,
    "editor.minimap.enabled": true,
    "editor.minimap.renderCharacters": true,
    "editor.autoIndent": true,
    "window.restoreWindows": "all",
    "window.openFoldersInNewWindow": "on",
    "editor.acceptSuggestionOnEnter": "off",
    "editor.tabCompletion": true,
    "emmet.triggerExpansionOnTab": true,

Pour un Python heureux

    "python.venvPath": "~/.local/share/virtualenvs/",
    "python.linting.mypyEnabled": true,
    "python.linting.enabled": true,
    "python.pythonPath": "/usr/bin/python3.6", # je l'override dans les settings de projet
    "black.path": "/home/user/.local/bin/black", # black a besoin de Python 3.6
    "python.formatting.provider": "none", # pour black
    "editor.formatOnPaste": true,
    "files.associations": {
        ".pylintrc": "ini"
    },
    "python.linting.flake8Enabled": true,
    "python.unitTest.pyTestEnabled": true,
    "python.linting.pylintEnabled": true,

J’installe généralement ces extensions

Astuces utiles

VSCode vient avec les raccourcis traditionnels des éditeurs graphiques: ctrl + F pour rechercher, ctrl + shift + f pour rechercher dans le projet, ctrl + d pour la sélection incrémentale, ctrl + p pour le “go to anywhere”, ctrl + shift + p pour le head up display, ctrl + s pour sauvegarder, ctrl (+shift) pour se balader (sélectioner) de mot en mot, etc.

Il possède aussi quelques trucs sympas dont on parle moins dans les tutos:

Un snippet perso que je rajoute également (dans $HOME/.config/Code/User/snippets/python.json):

   "wrap_in_try_except": {
        "prefix": "try",
        "body": [
          "try:",
          "\t${0}${TM_SELECTED_TEXT}",
          "except ${1:Exception}:",
          "\t${2:import pdb; pdb.set_trace()}"
        ],
        "description": "Wrap in try/except"
    },

Ça permet de sélectionner un truc, de taper try puis tab et avoir le tout wrappé dans un try/except.

Le futur

Depuis sa sortie, l’éditeur est en constante amélioration. Les mises à jour sont toujours une excellente surprise, avec des tas de goodies, y compris dans les extensions.

Mais là, dans la dernière version instable (qui a la bonne idée de ne pas overrider la stable à l’installation), VSCode vient avec une preview de l’édition collaborative. Genre Google doc, mais pour le code, et dans tout l’éditeur.

La partie chiante, c’est qu’il faut un compte (Microsoft évidemment), et donc que ça passe par leurs serveurs.

La partie amazing par contre, c’est que ça envoie du poney nucléaire. L’ouverture des onglets, l’écriture, le scroll… Tout se synchronise proprement. Si on décide de faire sa vie, VSCode désynchronise la navigation, et permet à tout le monde de travailler en parallèle sur le projet (et même optionnellement donner accès à son terminal). Si on veut de nouveau voir la navigation de l’autre, on peut demander de le suivre à nouveau, et pouf, on suit ce qu’il fait en live.

Testé avec des clients à des milliers de borne. C’est bluffant.

Vous l’avez compris

Trier un CSV de 5 Go 15

lundi 14 mai 2018 à 10:26

Marrant, j’ai jamais eu autant de RAM, et j’ai jamais eu autant de problèmes de RAM. On est en train de faire un bon dans inefficacité des programmes, et ça va pas aller en s’arrangeant entre docker, electron et nos chers navigateurs. Une grosse app Python peut devenir assez velue aussi niveau mémoire, même quand on n’est pas un boulet comme moi.

Et justement, en relisant un célèbre post de Guido sur la réponse à la blague “comment trier un million d’entiers avec 2M de Ram”, j’ai réalisé 2 choses:

Or c’est un peu ma raison d’être, si vous voulez, de prendre les trucs cools mais imbitables et les rendre utilisables.

Aujourd’hui, donc, on va trier un CSV de 5Go en Python. Ça parle plus qu’un fichier de nombres, et puis ça évite d’expliquer le module array qui est juste une optimisation.

J’ai pris mes petites mimines, et j’ai pondu un CSV qui contient 63Mo de:

A,01/02/2016,2
A,13/07/2011,1
B,24/01/1996,3
C,30/12/1999,1
D,13/07/2011,3
D,01/02/2016,5
E,24/01/1996,4
F,30/12/1999,1
G,13/07/2011,4
H,01/02/2016,4
I,01/02/2016,5
I,13/07/2011,2
A,01/02/2016,2
A,13/07/2011,1

En copier/coller.

Puis j’ai lancé un script pour dupliquer le bébé et jusqu’à atteindre 5,9 Go de taille:

with open('data.csv') as infile:
    with open('data2.csv', 'w') as outfile:
        for x in range(100):
            outfile.write(infile.read())
            infile.seek(0)
Si jamais vous doutiez que je vous aime...

Si jamais vous doutiez que je vous aime…

395366400 lignes. Jusqu’ici tout va bien.

Maintenant, supposons qu’on veuille trier sur la date. Si vos souvenirs en Python sont exacts (ou si vous avez lu notre super article sur Ordonner en Python), vous savez que la solution naïve est de loader le fichier cash pistache, puis d’appeler dessus sorted() avec le paramètre key.

D’abord, il faut choisir le callback à passer à key, c’est à dire la fonction qui va être exécutée pour chaque ligne pour extraire la date de la ligne et permettre ainsi à sorted() de comparer avec les autres dates.

>>> str_date = "A,01/02/2016,2".split(',')[1] # récupère la date uniquement
>>> str_date
'01/02/2016'
>>> ''.join(str_date.split('/')[::-1]) # on inverse la date avoir une valeur ordonnable
'20160201'

On en fait une fonction:

 
def extract_date(ligne):
    str_date = ligne.split(',')[1]
    return ''.join(str_date.split('/')[::-1])

Ensuite on a juste à ouvrir le fichier, trier les lignes, et sauvegarder tout ça dans l’autre fichier:

with open('data2.csv') as infile:
    with open('sorted_data.csv', 'w') as outfile:
        # on fait le tri
        sorted_lines = sorted(infile, key=extract_date)
        # on ecrit les lignes triées dans le nouveau fichier
        outfile.writelines(sorted_lines)

Easy money, double poney.

Enfin avec un fichier de quelques Mo. Parce que si vous faites ça dans un fichier de 5,9 Go, votre RAM va vomir comme une pom pom girl anorexique.

sorted() sur un disque complet, illustré

sorted() sur un disque complet, illustré

Comment résoudre ce problème ?

Et bien en faisant tout pareil, mais avec des petits morceaux !

import heapq

from tempfile import TemporaryFile
from itertools import islice

# On garde notre fonction key
def extract_date(ligne):
    str_date = ligne.split(',')[1]
    return ''.join(str_date.split('/')[::-1])

# Liste de tous les fichiers temporaires avec les lignes triées
sorted_temp_files = []

with open('data2.csv') as infile:
    progress = 0
    while True:
        # On lit seulement 3000000 lignes sur 395366400 à chaque tour de boucle
        lines = list(islice(infile, 3000000))

        if not lines:  # plus de ligne ? On sort
            break

        # On affiche où on en est
        print("{:.2f}%".format(progress))
        progress += (3000000 / 395366400 * 100)

        # On tri les lignes, comme avec sorted() mais sur place. 
        # Ça évite de dupliquer les données en mémoire.
        lines.sort(key=extract_date)

        # On crée un fichier temporaire qui va contenir les 3000000 lignes
        # triées
        f = TemporaryFile(mode="r+")
        f.writelines(lines)

        # On rembobine le fichier pour pouvoir relire le contenu plus tard
        f.seek(0)

        # On balance le fichier dans la liste des fichiers triés
        # L'objet fichier hein. Pas le chemin du fichier. C'est pour ça qu'on
        # a fait .seek(0) avant. On va en avoir besoin plus bas.
        sorted_temp_files.append(f)

    # Toute la magie se fait là.
    # On utilise heapq.merge(), qui prend en paramètre plein de trucs qu'on 
    # vient de trier, et permet de se balader dedans comme si c'était un seul
    # gros truc trié, mais sans tout charger en mémoire.
    # En gros il regarde les premières valeurs de tous les itérables, les compares,
    # puis fait retourne un générateur qui yield tout dans l'ordre 
    with open('sorted_data.csv', 'w') as outfile:
        for ligne in heapq.merge(*sorted_temp_files, key=extract_date):
            outfile.write(ligne)

Au lieu de charger les 5 Go en mémoire, on plafonne à 400 Mo. Une raison de plus d’apprendre à utiliser les générateurs.

Sametmax, c'est de la bombe

Sametmax, c’est de la bombe

Alors évidemment, c’est long. Y a genre 40 minutes de traitement. Moins si on utilise pypy et qu’on accepte de up la conso RAM. On code pas en Rust :)

Si vous avez besoin de perfs et que Python reste votre outil de prédilections, 3 solutions restent à votre disposition:

– Prétraiter le fichier en lui rajoutant une première colonne qui contient un timestamp. Ca c’est super rapide à trier.
– Utiliser un truc de numpy ou pandas comme np.memmap().sort(kind=’merge’). C’est du C, donc ça speed.
Acheter de la ram et tout trier en mémoire avec la solution 1.

EDIT:

Un lecteur m’a alpagué sur twitter pour me dire qu’on peut faire ça plus vite et plus facilement avec sort. Oui, évidement.

L’exercice est académique, il est simplifié pour vous permettre de comprendre heapq.merge(). En réalité, on aura vraiment besoin de ça que pour un cas complexe. Exemple, vous lisez un flux de log une socket et vous checkez les adresses IP d’abord pour filtrer celles de la liste noire, puis pour les faire matcher une zone géographique et vous triez tout ça et vous le passez à la suite du pipeline. Et le tout doit marcher sous Linux et Windows Server avec juste la lib standard parce que diab est de mauvais poil.

Évidement que si vous voulez juste trier un CSV vous n’allez pas coder un script python de 30 lignes.

13558 Go de rames 25

jeudi 15 mars 2018 à 07:01

J’ai une tache celery qui me génère des données de test. Elle est lancée toutes les 5 minutes pour simuler le crawling d’un site qui popule une base de données, le tout piloté par l’ORM django, puis dumpé dans redis.

Jusqu’ici tout va bien.

Mais après un certain temps, ma machine rame, puis se frise.

Et j’ai du mal à y croire. J’ai 8 coeurs, un 32 Go de mémoire vive, un SSD d’un putain de To. On va pas me la faire à l’envers, c’est pas un def tout moisi qui va me faire trembler les genoux. C’est forcément un de ces cons de wallets que j’ai laissé ouvert, encore codé par un nantais ça !

Mais après une enquête minutieuse, qui a consisté en subtilement killer tous mes processus un par un avec echo "douceur" | sed s/c/l, le constat est là.

Eukekaca

Eukekaca.

Cette fonction est responsable:

def generate_fake_stats(x=1000):

    cur_stats = {
        s.currency.short_code: s
        for s in CurrencyMarketStatsFactory.build_batch(x)
    }
    
    mn_stats: Dict[str, MNMarketStats] = {
        s.masternode.coin.short_code: s
        for s in MNMarketStatsFactory.build_batch(x)
    }

    stats = []

    for code, mns in mn_stats.items():

        cstats: CurrencyMarketStats = cur_stats.get(code)

        if not cstats:
            continue

        dollar_value = float(cstats.dollar_value),
        collateral = mns.masternode.collateral

        stats = {
            'created': int(mns.created.timestamp()),
            'name': cstats.currency.name,
            'code': code,
            'title': f'{cstats.currency.name} ({code})',
            'marketcap': dollar_value * cstats.supply,
            'dollar_value': dollar_value,
            'change_rate': cstats.change_rate,
            'volume': float(cstats.volume),
            'supply': cstats.supply,
            'roi': mns.roi,
            'mn_worth': collateral * dollar_value,
            'node_count': mns.node_count,
            'required_coins': collateral,
        }

    RedisClient.get_instance().jset('marketstats', stats)
    return stats

Il m’a fallu une bonne heure pour trouver ma connerie. J’ai changé plein d’options, mis DEBUG sur False, limité la mémoire de Redis, etc.

Mais non, c’était mon code. Qui générait au bas mot 847390982*1000*16 octets de données, soit 1,355825571×10¹³ pour mes objets Python.

J'ai un nouveau mapping clavier qui permet de n'utiliser qu'un doigt

J’ai un nouveau mapping clavier qui permet de n’utiliser qu’un doigt

Il y en a un peu plus madame, je vous le mets quand même ?

Nan parce que 13 To pour une pov liste de dicos, c’est la boucherie.

Alors, sachant que je vous ai éliminé la recherche des causes parallèles et que vous savez que le bug est de ma (grande et stupide) faute, saurez-vous trouver dans ces lignes le d20 qui tombe sur un 1 à tous les jets ?

Si vous ne trouvez pas, la réponse dans quelques jours.

Explain all the humans !

Explain all the humans !

EDIT pour la réponse:

Comme plusieurs personnes l’ont compris, c’est la virgule sur:

    dollar_value = float(cstats.dollar_value),

Qui est responsable de tout ce malheur.

En effet plus loin on fait:

    'mn_worth': collateral * dollar_value,

Ce qui, au lieu de multiplier un entier par un float, multiplie un entier par un tuple. En python, c’est légal, et ça donne ça:

>>> 10 * (7808979.8989,)
(7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989)

Si collateral est élevé, ce qui est ici mon cas, ça fait de très gros tuples, et le tout dans une boucle.