[PYTHON] Utilisez la barre de progression avec Click: (Facile à utiliser, améliore l'affichage des tâches qui prennent plus de 24 heures, notes lors de l'utilisation en traitement parallèle)

Cliquez sur mignon Cliquez

Une fois que vous connaissez la commodité de la Click Library pour créer des applications console en Python, c'est argparse ou optparse, donc vous ne savez même pas ce que signifie analyser des arguments. droite.

Pour Click, Site officiel ou un article d'introduction par ikuyamada "Créez facilement une application console Python avec Click" Comme tu vois.

Écrivez sur la barre de progression de Click.

En affichant l'avancement du travail qui prend un peu de temps avec une barre, il réduit le stress du temps d'attente pour l'utilisateur.

Envelopper iterable avec `` click.progressbar () '' vous donnera les éléments un par un et en même temps mettra à jour joliment la barre de progression.

échantillon

À titre d'exemple simple, j'ai écrit une application qui compte à rebours avec la commande OS X say. Il prend en charge quatre langues, anglais, allemand, français et japonais. (La voix de chaque langue doit être installée) // Si vous n'êtes pas Macker, veuillez modifier la fonction say comme il convient.

countdown.py


#!/usr/bin/env python
#
# countdown
#
import click
import time
import os

_lang = 'en'

def say(num):
    if _lang == 'ja':
        voice = 'Kyoko'
    elif _lang == 'fr':
        voice = 'Virginie'
    elif _lang == 'de':
        voice = 'Anna'
    else:
        voice = 'Agnes'

    os.system('say --voice %s %d' % (voice, num))


@click.command()
@click.argument('count', type=int)
@click.option('--lang', default='en')
def countdown(count, lang):
    global _lang
    _lang = lang

    numbers = range(count, -1, -1)
    with click.progressbar(numbers) as bar:
        for num in bar:
            say(num)
            time.sleep(1)


if __name__ == '__main__':
    countdown()

Je vais le déplacer.

$ python countdown.py 10 --lang=ja

Lorsque le compte à rebours à partir de 10 commence, la barre de progression s'affiche comme ceci.

[##################------------------]   50%  00:00:06

Le temps restant est estimé à partir de la progression jusqu'à ce point et est affiché à l'extrémité droite.

options click.progressbar ()

click.progressbar(iterable=None, length=None, label=None, show_eta=True, show_percent=None, show_pos=False, item_show_func=None, fill_char='#', empty_char='-', bar_template='%(label)s [%(bar)s] %(info)s', info_sep=' ', width=36, file=None, color=None)

Il y a une explication dans le document original http://click.pocoo.org/5/api/#click.progressbar, mais pour le moment.

--iterable: Itérable pour être itérable. Si aucun, une spécification de longueur est requise. --length: le nombre d'éléments à itérer. Si iterable est passé, il demandera la longueur par défaut (bien que la longueur puisse ne pas être connue). Si le paramètre de longueur est spécifié, il effectue une itération pour la longueur spécifiée (pas la longueur itérable). --label: le libellé à afficher à côté de la barre de progression. --show_eta: s'il faut afficher le temps estimé requis Si la longueur ne peut pas être déterminée, elle est automatiquement masquée. --show_percent: indique s'il faut afficher le pourcentage. (La valeur par défaut est True si itérable a une longueur, False sinon) --show_pos: affiche la position absolue (la valeur par défaut est False) --item_show_func: Fonction appelée lors de l'affichage de l'élément en cours de traitement. Cette fonction renvoie la chaîne qui doit être affichée dans la barre de progression de l'élément en cours de traitement. Notez que l'élément peut être Aucun. --fill_char: Le caractère utilisé pour la partie de remplissage de la barre de progression. --empty_char: Le caractère utilisé pour la partie non remplie de la barre de progression. --bar_template: Chaîne de format utilisée comme modèle de barre. Les trois paramètres qui y sont utilisés sont l'étiquette, la barre et les informations. --info_sep: séparateur pour plusieurs éléments d'information (eta, etc.). --width: La largeur de la barre de progression (en unités de caractères demi-largeur). Si 0 est spécifié, la largeur totale du terminal --file - Ecrit le fichier de destination. Seule l'étiquette est affichée sauf pour le terminal. --color: le terminal prend-il en charge les couleurs ANSI? (Identification automatique)

~~ Piège: je ne sais pas combien de jours cela prendra si le temps restant est de 24 heures ou plus ~~

  • 2015/11/8 Il s'agit d'un problème avec la version actuelle (5.1) au moment de la rédaction. J'ai envoyé une demande de tirage à la famille principale, il se peut donc que le problème ait été résolu au moment où vous lirez l'article. → [La demande d'extraction a été fusionnée] la nuit du 10 novembre 2015 (https://github.com/mitsuhiko/click/pull/453#event-460027353). Le maître actuel a résolu ce problème. Cependant, je pense que certaines personnes (version <= 5.1) ne peuvent pas utiliser la version master pour le moment, je laisse donc les contre-mesures suivantes.

C'est une fonction très pratique, mais il y a un écueil.

À titre de test, effectuons un compte à rebours à partir de 1 million avec l'échantillon précédent.

$ python countdown.py 1000000
[------------------------------------]    0%  00:20:47

20 minutes pour aller à 999999? Chaque fois que j'en compte un, je mets une seconde de sommeil, donc cela devrait prendre 11 jours et 13 heures 46 minutes au plus tôt.

Que voulez-vous dire?

Si le temps restant est de 24 heures ou plus, l'affichage reviendra à 00:00:00. En d'autres termes, seul le reste du temps restant divisé par 24 heures est affiché.

Jetons un coup d'œil à la source du clic.

Dans la classe ProgressBar (définie dans click / _termui_impl.py '') Le temps nécessaire (eta) est formaté par une fonction membre appelée format_eta () ''.

    @property
    def eta(self):
        if self.length_known and not self.finished:
            return self.time_per_iteration * (self.length - self.pos)
        return 0.0

    def format_eta(self):
        if self.eta_known:
            return time.strftime('%H:%M:%S', time.gmtime(self.eta + 1))
        return ''

Il est implémenté comme ça. Il est triste que seules les tâches qui peuvent être accomplies dans les 24 heures soient attendues.

Dois-je changer ici '% H:% M:% S' en '% d% H:% M:% S'? Ce n'est pas.

$ python
Python 2.7.9 (default, Jan  7 2015, 11:50:42) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import time
>>> time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(0))
'1970-01-01 00:00:00'
>>> time.strftime('%H:%M:%S', time.gmtime(0))
'00:00:00'
>>> time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(60*60*24))
'1970-01-02 00:00:00'
>>> time.strftime('%H:%M:%S', time.gmtime(60*60*24))
'00:00:00'
```

 `` time.gmtime (t) '' est une structure composée de 9 éléments: année, mois, jour, heure, minute, seconde, jour, quel jour de l'année et heure d'été. Je retourne `time.struct_time``
 Puisqu'il est basé sur l'époque Unix (1er janvier 1970 0:00:00), `` time.gmtime (0) '' est "1 janvier 1970" 0:00:00 En quelques secondes, lancez ceci dans `` time.stftime () '' et prenez ``% Y-% m-% d`` pour obtenir "1970-01-01".
 (La raison pour laquelle l'implémentation de `` format_eta () '' fonctionne avec `` '% H:% M:% S'`` est parce que l'époque unix n'est pas à mi-chemin à 00:00:00. )

 Donc, si vous considérez l'unité quotidienne, il semble préférable de calculer par vous-même sans compter sur `` time.gmtime '' ou `` time.strftime ''.

 Par exemple:

```
def format_eta_impl(self):
    if self.eta_known:
        t = self.eta
        seconds = t % 60
        t /= 60
        minutes = t % 60
        t /= 60
        hours = t % 24
        t /= 24
        if t > 0:
            days = t
            return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds)
        else:
            return '%02d:%02d:%02d' % (hours, minutes, seconds)
    else:
        return ''
```

 Je voudrais remplacer la fonction membre `` format_eta (self) '' de la classe ProgressBar par ceci, mais comme la classe ProgressBar elle-même est définie dans click / _termui_impl.py, elle est invisible pour les utilisateurs de la bibliothèque.

```
    with click.progressbar(numbers) as bar:
        bar.__class__.format_eta = format_eta_impl
        for num in bar:
            say(num)
            time.sleep(1)
```

 Il semble que cela devrait être fait comme ça.
 Cela ne fonctionne pas avec `` bar.format_eta = format_eta_impl ''.
 (Je vais omettre l'explication de ce domaine ici!)

 Utilisons-le.

```
$ python countdown.py 1000000
[------------------------------------]    0%  18d 23:20:12
```

 Le compte à rebours de One Million a commencé.
 18 jours restants et 23 heures 20 minutes 12 secondes ww


 alors.
 (´-`) .. oO (Vous devez l'écrire correctement et l'envoyer au chef de famille Cliquez)
 → [Envoyé](https://github.com/mitsuhiko/click/pull/453)
 → [Fusionné](https://github.com/mitsuhiko/click/pull/453#event-460027353)

## countdown.py (version améliorée)


#### **`big_countdown.py`**
```py

#!/usr/bin/env python
#
# countdown (for big number)
#
import click
import time
import os

_lang = 'en'

def say(num):
    if _lang == 'ja':
        voice = 'Kyoko'
    elif _lang == 'fr':
        voice = 'Virginie'
    elif _lang == 'de':
        voice = 'Anna'
    else:
        voice = 'Agnes'

    os.system('say --voice %s %d' % (voice, num))


def format_eta_impl(self):
    if self.eta_known:
        t = self.eta
        seconds = t % 60
        t /= 60
        minutes = t % 60
        t /= 60
        hours = t % 24
        t /= 24
        if t > 0:
            days = t
            return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds)
        else:
            return '%02d:%02d:%02d' % (hours, minutes, seconds)
    else:
        return ''


@click.command()
@click.argument('count', type=int)
@click.option('--lang', default='en')
def countdown(count, lang):
    global _lang
    _lang = lang

    numbers = range(count, -1, -1)
    with click.progressbar(numbers) as bar:
        bar.__class__.format_eta = format_eta_impl
        for num in bar:
            say(num)
            time.sleep(1)

if __name__ == '__main__':
    countdown()
```

## Précautions lors de l'utilisation de click.progressbar pour les tâches à traiter en parallèle

### Symptômes

 ――La barre de progression progresse d'environ la moitié au début même si rien ne progresse
 ――La tâche n'est pas terminée pour toujours, même si l'affichage du temps restant est d'environ 1 à 2 secondes.

 Pour ceux qui ont des problèmes avec ces symptômes.

 Par exemple, si vous utilisez multiprocessing.pool.Pool, l'affichage de la progression de la barre de progression sera incrémenté à "lorsque le processus est placé dans le pool" (plutôt que lorsque le processus a réellement eu lieu).

 Pour cette raison, il se produit un phénomène dans lequel la barre de progression avance d'abord d'environ la moitié même si aucun traitement n'a progressé.

 La barre de progression semble avoir terminé environ la moitié du traitement au premier instant, de sorte que le temps de traitement restant est estimé à 1 ou 2 secondes et affiché. Ceci est la cause du phénomène que la tâche n'est pas terminée pour toujours même si l'affichage du temps restant est d'environ 1 à 2 secondes.

### Contre-mesures

 Pour pouvoir estimer le temps restant le plus précisément possible en faisant avancer l'affichage de la progression au moment où il a été effectivement traité

 --Set length = (size of iterable) dans l'argument de click.progressbar () (pas iterable lui-même)
 --Appelez bar.update (1) chaque fois que vous en traitez un

### Ctrl-C kill ne fonctionne pas avec le multitraitement

 Ce n'est pas la faute de Click, mais ... de créer une CLI facile à utiliser.
 (Il y a multiprocessing.pool.Pool)
 Le pool a détecté le KeyboardInterrupt, donc configurons l'initialiseur pour ignorer le KeyboardInterrupt du côté du pool.

### Exemple d'implémentation

 J'ai essayé de paralléliser l'échantillon de lecture du compte à rebours.
 (Ça fait du mal)


#### **`countdown_mp.py`**
```py

import click
import time
import os
import multiprocessing
from multiprocessing.pool import Pool
from functools import partial
import signal

_lang = 'en'

def say(num):
    if _lang == 'ja':
        voice = 'Kyoko'
    elif _lang == 'fr':
        voice = 'Virginie'
    elif _lang == 'de':
        voice = 'Anna'
    else:
        voice = 'Agnes'

    os.system('say --voice %s %d' % (voice, num))
    time.sleep(1)

    return num


def _init_worker():
    signal.signal(signal.SIGINT, signal.SIG_IGN)


@click.command()
@click.argument('count', type=int)
@click.option('--lang', default='en')
@click.argument('pool-size', type=int, default=multiprocessing.cpu_count())
@click.argument('chunk-size', type=int, default=5)
@click.argument('max-tasks-per-child', type=int, default=3)
def countdown(count, lang, pool_size, chunk_size, max_tasks_per_child):
    global _lang
    _lang = lang

    pool = Pool(pool_size, _init_worker, maxtasksperchild=max_tasks_per_child)
    imap_func = partial(pool.imap, chunksize=chunk_size)

    numbers = range(count, -1, -1)

    with click.progressbar(length=len(numbers)) as bar:
        for result in imap_func(say, numbers):
            bar.update(1)

    pool.close()


if __name__ == '__main__':
    countdown()
```


Recommended Posts

Utilisez la barre de progression avec Click: (Facile à utiliser, améliore l'affichage des tâches qui prennent plus de 24 heures, notes lors de l'utilisation en traitement parallèle)
Affichage en temps réel de la progression du traitement côté serveur sur le navigateur (implémentation de la barre de progression)
Recevez une liste des résultats du traitement parallèle en Python avec starmap