Si vous souhaitez créer une application Windows (exe) qui peut être utilisée maintenant en utilisant uniquement Python

introduction

Cet article est basé sur le thème du Qiita Summer Festival 2020 "Si vous souhaitez créer une △△ (application) en utilisant uniquement 〇〇 (langue)" Le contenu y est conforme.

Le 5 juillet 2020, mon travail "VMD sizing ver5.00 (execode)" Est sorti. Le thème de cet outil est de "régénérer VMD (données de mouvement MMD) avec la tête et le corps appropriés pour le modèle spécifié", eh bien, cela semble à la plupart d'entre vous qui lisez cet article. C'est une application de loisir complète.

** Nga! !! !! ** **

La plupart des gens qui aiment MMD (MikuMikuDance) sont des gens très ordinaires qui n'ont rien à voir avec Python ou des programmes. Si quelqu'un répond par "serpent?!", Cela peut même sembler une existence précieuse. Comment ces personnes peuvent-elles utiliser leurs propres applications? Cet article traite du thème de la création d'une ** "App qui s'ajuste à une sensation agréable une fois que l'interface utilisateur est duveteuse" ** avec Python, qui est apparu après une telle angoisse, des essais et des erreurs et beaucoup de problèmes. , C'est un résumé de mes propres réponses. Par ailleurs, le "dimensionnement VMD" a été utilisé dans la mesure où le total cumulé dépasse DL8500. (Environ 9600 DL en combinaison avec la version 32 bits)

Python + pyinstaller = exe n'est pas si rare, mais il faut une certaine ingéniosité pour le ramener à un niveau qui puisse supporter un fonctionnement réel.

<"N'est-il pas acceptable de le faire en C?" ** J'aime Python. ** (Parce que je ne sais pas C ...)

1. Construction de l'environnement

1.1. Installation d'Anaconda

Tout d'abord, préparons l'environnement de développement.

<"Ce n'est pas une raison d'apprendre par machine, n'est-ce pas normal de le laisser brut?" **En aucune façon! !! ** **

Un problème courant dans les articles pyinstaller est que "des bibliothèques supplémentaires sont incluses et le fichier exe devient volumineux". Il est courant d'essayer de nouvelles bibliothèques pendant le développement. Cependant, si vous créez un exe tel quel, il y a de fortes chances que des bibliothèques inutiles soient incluses dans l'exe. ** Séparons exactement l'environnement de développement et l'environnement de publication. ** **

Téléchargez le programme d'installation depuis Anaconda Official. S'il est fait désormais, il vaudrait mieux utiliser 3 séries.

image.png

Après DL, suivez les étapes pour installer.

1.2. Créer un environnement de développement

Tout d'abord, construisons un environnement virtuel pour le développement.

conda create -n pytest_env pip python=3.7

Une fois que vous avez un environnement de développement, «activons». Au fait, créons également un répertoire de gestion pour le code source.

1.3. Création d'un environnement de publication

De même, créez un environnement de version,

conda create -n pytest_release pip python=3.7

1.4. Installation de la bibliothèque

Une fois que vous avez le répertoire de gestion, accédez-y et installez les bibliothèques nécessaires. Voici une astuce.

** pyinstaller s'installe uniquement dans l'environnement de version **

En installant pyinstaller uniquement dans l'environnement de version, vous pouvez éviter les erreurs que vous publiez accidentellement dans l'environnement de développement. Puisque c'est un gros problème, introduisons numpy.

Commande d'installation pour l'environnement de développement

pip install numpy wxPython

Commande d'installation pour l'environnement de version

pip install numpy wxPython pypiwin32 pyinstaller

pypiwin32 semble être la bibliothèque nécessaire pour exécuter pyinstaller sous Windows.

L'interface graphique est facile à créer à l'aide de WxFormBuilder. Cependant, il y a certaines choses qui sont difficiles à comprendre la convention de dénomination automatique, il n'est pas possible de réutiliser des pièces, etc., et il y a certaines choses qui ne sont pas suffisantes pour créer une application d'opération réelle, donc je la produis quand elle est sous une certaine forme, et après cela je le ferai moi-même Je le recommande.

Référence: GUI (WxFormBuilder) en Python (mm_sys) https://qiita.com/mm_sys/items/716cb159ea8c9e634300

Après cela, nous procéderons dans un format de pull inversé. Veuillez consulter la section qui vous intéresse.

3. collection TIPS inverse exe avec python

3.1. Comment déplacer le thread logique tout en exécutant le thread GUI avec une fonction de suspension

Je pense que la plupart des personnes intéressées par cet article s'y intéressent. J'aimerais savoir s'il y a une réponse correcte. J'ai donc sauté diverses choses et je les ai apportées au début.

--Démarrez un thread logique qui s'exécute pendant une longue période tout en gardant le thread GUI tel quel

Vous trouverez ci-dessous le code qui répond aux exigences ci-dessus.

executor.py


# -*- coding: utf-8 -*-
#

import wx
import sys
import argparse
import numpy as np
import multiprocessing
from pathlib import Path

from form.MainFrame import MainFrame
from utils.MLogger import MLogger

VERSION_NAME = "ver1.00"

#Pas de notation d'exposant, omis si le nombre de chiffres décimaux effectifs dépasse 6, 30 caractères, 200 caractères par ligne
np.set_printoptions(suppress=True, precision=6, threshold=30, linewidth=200)

#Mesures multi-processus Windows
multiprocessing.freeze_support()


if __name__ == '__main__':
    #Interprétation des arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("--verbose", default=20, type=int)
    args = parser.parse_args()
    
    #Initialisation de l'enregistreur
    MLogger.initialize(level=args.verbose, is_file=False)

    #Démarrage de l'interface graphique
    app = wx.App(False)
    frame = MainFrame(None, VERSION_NAME, args.verbose)
    frame.Show(True)
    app.MainLoop()

Tout d'abord, ʻexecutor.py` de l'appelant. Lancez l'interface graphique à partir d'ici.

MainFrame.py


# -*- coding: utf-8 -*-
#

from time import sleep
from worker.LongLogicWorker import LongLogicWorker
from form.ConsoleCtrl import ConsoleCtrl
from utils.MLogger import MLogger

import os
import sys
import wx
import wx.lib.newevent

logger = MLogger(__name__)
TIMER_ID = wx.NewId()

(LongThreadEvent, EVT_LONG_THREAD) = wx.lib.newevent.NewEvent()

#Interface graphique principale
class MainFrame(wx.Frame):

    def __init__(self, parent, version_name: str, logging_level: int):
        self.version_name = version_name
        self.logging_level = logging_level
        self.elapsed_time = 0
        self.worker = None

        #Initialisation
        wx.Frame.__init__(self, parent, id=wx.ID_ANY, title=u"c01 Long Logic {0}".format(self.version_name), \
                          pos=wx.DefaultPosition, size=wx.Size(600, 650), style=wx.DEFAULT_FRAME_STYLE)

        self.sizer = wx.BoxSizer(wx.VERTICAL)

        #temps de traitement
        self.loop_cnt_ctrl = wx.SpinCtrl(self, id=wx.ID_ANY, size=wx.Size(100, -1), value="2", min=1, max=999, initial=2)
        self.loop_cnt_ctrl.SetToolTip(u"temps de traitement")
        self.sizer.Add(self.loop_cnt_ctrl, 0, wx.ALL, 5)

        #Case à cocher pour le traitement parallèle
        self.multi_process_ctrl = wx.CheckBox(self, id=wx.ID_ANY, label="Si vous souhaitez exécuter un traitement parallèle, veuillez le vérifier.")
        self.sizer.Add(self.multi_process_ctrl, 0, wx.ALL, 5)

        #Bouton Sizer
        self.btn_sizer = wx.BoxSizer(wx.HORIZONTAL)

        #Bouton Exécuter
        self.exec_btn_ctrl = wx.Button(self, wx.ID_ANY, u"Démarrer le traitement logique long", wx.DefaultPosition, wx.Size(200, 50), 0)
        #Lier avec l'événement de clic gauche de la souris [Point.01】
        self.exec_btn_ctrl.Bind(wx.EVT_LEFT_DOWN, self.on_exec_click)
        #Lier avec l'événement de double-clic gauche de la souris [Point.03】
        self.exec_btn_ctrl.Bind(wx.EVT_LEFT_DCLICK, self.on_doubleclick)
        self.btn_sizer.Add(self.exec_btn_ctrl, 0, wx.ALIGN_CENTER, 5)

        #Bouton de suspension
        self.kill_btn_ctrl = wx.Button(self, wx.ID_ANY, u"Longue interruption du traitement logique", wx.DefaultPosition, wx.Size(200, 50), 0)
        #Lier avec l'événement de clic gauche de la souris
        self.kill_btn_ctrl.Bind(wx.EVT_LEFT_DOWN, self.on_kill_click)
        #Lier avec l'événement de double clic gauche de la souris
        self.kill_btn_ctrl.Bind(wx.EVT_LEFT_DCLICK, self.on_doubleclick)
        #L'état initial est inactif
        self.kill_btn_ctrl.Disable()
        self.btn_sizer.Add(self.kill_btn_ctrl, 0, wx.ALIGN_CENTER, 5)

        self.sizer.Add(self.btn_sizer, 0, wx.ALIGN_CENTER | wx.SHAPED, 0)

        #Console [Point.06】
        self.console_ctrl = ConsoleCtrl(self)
        self.sizer.Add(self.console_ctrl, 1, wx.ALL | wx.EXPAND, 5)

        #print La destination de sortie est la console [Point.05】
        sys.stdout = self.console_ctrl

        #Jauge de progression
        self.gauge_ctrl = wx.Gauge(self, wx.ID_ANY, 100, wx.DefaultPosition, wx.DefaultSize, wx.GA_HORIZONTAL)
        self.gauge_ctrl.SetValue(0)
        self.sizer.Add(self.gauge_ctrl, 0, wx.ALL | wx.EXPAND, 5)

        #Liaison d'événement [Point.05】
        self.Bind(EVT_LONG_THREAD, self.on_exec_result)

        self.SetSizer(self.sizer)
        self.Layout()

        #Affichage au centre de l'écran
        self.Centre(wx.BOTH)

    #Double-cliquez sur le processus d'invalidation
    def on_doubleclick(self, event: wx.Event):
        self.timer.Stop()
        logger.warning("Il a été double-cliqué.", decoration=MLogger.DECORATION_BOX)
        event.Skip(False)
        return False

    #Exécution 1 Traitement en cas de clic
    def on_exec_click(self, event: wx.Event):
        #Commencez avec un léger retard avec une minuterie (évitez de battre avec un double clic) [Point.04】
        self.timer = wx.Timer(self, TIMER_ID)
        self.timer.StartOnce(200)
        self.Bind(wx.EVT_TIMER, self.on_exec, id=TIMER_ID)

    #Interruption de traitement en 1 clic
    def on_kill_click(self, event: wx.Event):
        self.timer = wx.Timer(self, TIMER_ID)
        self.timer.StartOnce(200)
        self.Bind(wx.EVT_TIMER, self.on_kill, id=TIMER_ID)

    #Exécution du traitement
    def on_exec(self, event: wx.Event):
        self.timer.Stop()

        if not self.worker:
            #Console claire
            self.console_ctrl.Clear()
            #Désactiver le bouton d'exécution
            self.exec_btn_ctrl.Disable()
            #Bouton de suspension activé
            self.kill_btn_ctrl.Enable()

            #Exécuter dans un autre thread [Point.09】
            self.worker = LongLogicWorker(self, LongThreadEvent, self.loop_cnt_ctrl.GetValue(), self.multi_process_ctrl.GetValue())
            self.worker.start()
            
        event.Skip(False)

    #Suspendre l'exécution du traitement
    def on_kill(self, event: wx.Event):
        self.timer.Stop()

        if self.worker:
            #Lorsque le bouton est enfoncé à l'état arrêté, il s'arrête
            self.worker.stop()

            logger.warning("Interrompt le traitement logique long.", decoration=MLogger.DECORATION_BOX)

            #Fin ouvrier
            self.worker = None
            #Bouton Exécuter activé
            self.exec_btn_ctrl.Enable()
            #Désactiver le bouton de suspension
            self.kill_btn_ctrl.Disable()
            #Masquer la progression
            self.gauge_ctrl.SetValue(0)

        event.Skip(False)
    
    #Traitement après une longue logique terminée
    def on_exec_result(self, event: wx.Event):
        # 【Point.12] Faire connaître explicitement la fin logique
        self.sound_finish()
        #Bouton Exécuter activé
        self.exec_btn_ctrl.Enable()
        #Désactiver le bouton de suspension
        self.kill_btn_ctrl.Disable()

        if not event.result:
            event.Skip(False)
            return False
        
        self.elapsed_time += event.elapsed_time
        logger.info("\n Temps de traitement: %s", self.show_worked_time())

        #Fin ouvrier
        self.worker = None
        #Masquer la progression
        self.gauge_ctrl.SetValue(0)

    def sound_finish(self):
        #Sonner le son de fin
        if os.name == "nt":
            # Windows
            try:
                import winsound
                winsound.PlaySound("SystemAsterisk", winsound.SND_ALIAS)
            except Exception:
                pass

    def show_worked_time(self):
        #Convertir les secondes écoulées en heures, minutes et secondes
        td_m, td_s = divmod(self.elapsed_time, 60)

        if td_m == 0:
            worked_time = "{0:02d}Secondes".format(int(td_s))
        else:
            worked_time = "{0:02d}Minutes{1:02d}Secondes".format(int(td_m), int(td_s))

        return worked_time

Point.01: Lier l'événement de clic gauche de la souris et la méthode d'exécution

Tout d'abord, lions l'événement de clic gauche de la souris sur le bouton et la méthode à exécuter. Il y a plusieurs façons de lier, mais j'aime personnellement la façon de lier des parties et des événements de l'interface graphique en tant que Parts.Bind (type d'événement, méthode de déclenchement) car c'est facile à comprendre.

Point.02: Démarrez l'événement du clic gauche avec un léger retard de la minuterie

Si vous prenez l'événement de clic gauche et que vous l'exécutez tel quel, il se déclenchera en même temps que l'événement de double-clic et, par conséquent, l'événement de double-clic s'exécutera. (En termes de traitement, l'événement de double-clic fait partie de l'événement de simple clic, donc les deux événements se déclenchent en même temps.) Par conséquent, en définissant une minuterie dans ʻon_exec_click et ʻon_kill_click déclenchée d'un simple clic et en l'exécutant avec un léger retard, la méthode d'exécution liée à l'événement de double-clic sera exécutée en premier.

Point.03: Lier l'événement de double-clic gauche de la souris et la méthode d'exécution

Liez l'événement de double-clic gauche dans la même procédure que Point ①. Vous pouvez éviter le double traitement en effectuant des doubles clics ici.

Point.04: Arrêtez l'événement timer avec la méthode d'exécution du double-clic gauche de la souris

L'événement de double-clic arrêtera l'événement de minuterie exécuté à Point②. Vous pouvez maintenant désactiver le double-clic.

Seul l'événement en un seul clic se déclenche … L'événement correspondant sera exécuté avec un léger retard

image.png

Lorsque l'événement de double-clic se déclenche … L'événement de simple clic n'est pas exécuté car le minuteur est arrêté.

image.png

Point.05: Définissez la destination de sortie de print sur le contrôle de la console

print est un wrapper pour sys.stdout.write, donc si vous définissez la destination de sortie sur un contrôle de console, la destination de sortie de print sera à l'intérieur du contrôle.

Point 06: Définir le contrôle de la console dans la sous-classe

Alors, quel est ce contrôle de console? C'est une sous-classe de wx.TextCtrl.

ConsoleCtrl.py


# -*- coding: utf-8 -*-
#

import wx
from utils.MLogger import MLogger # noqa

logger = MLogger(__name__)


class ConsoleCtrl(wx.TextCtrl):

    def __init__(self, parent):
        #Plusieurs lignes autorisées, en lecture seule, sans bordure, avec défilement vertical, avec défilement horizontal, avec acquisition d'événements clés
        super().__init__(parent, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(-1, -1), \
                         wx.TE_MULTILINE | wx.TE_READONLY | wx.BORDER_NONE | wx.HSCROLL | wx.VSCROLL | wx.WANTS_CHARS)
        self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT))
        #Liaison d'événements de clavier
        self.Bind(wx.EVT_CHAR, lambda event: self.on_select_all(event, self.console_ctrl))

    #Traitement de sortie de la partie console [Point.07】        
    def write(self, text):
        try:
            wx.CallAfter(self.AppendText, text)
        except: # noqa
            pass

    #Tout le processus de sélection de la partie console [Point.08】
    def on_select_all(event, target_ctrl):
        keyInput = event.GetKeyCode()
        if keyInput == 1:  # 1 stands for 'ctrl+a'
            target_ctrl.SelectAll()
        event.Skip()

Point.07: effectuer un traitement supplémentaire dans la méthode write

Appelez la méthode ʻAppendText avec CallAfter`, en supposant qu'elle sera appelée à partir d'un thread logique différent du thread GUI. Cela stabilisera également la sortie «print» du thread logique.

Point.08: Ajouter une méthode pour tous les événements sélectionnés dans le contrôle de la console

S'il y a des lettres, c'est la saga humaine qui donne envie de les copier. Par conséquent, le processus de sélection totale est exécuté dans l'événement de sélection totale (combinaison d'événements de clavier).

Point.09: Exécuter le thread logique dans un autre thread

Je suis finalement entré dans le sujet principal. Effectuez un traitement logique dans LongLogicWorker. Source de référence: https://doloop while.hatenablog.com/entry/20090627/1275175850

LongLogicWorker.py


# -*- coding: utf-8 -*-
#

import os
import wx
import time
from worker.BaseWorker import BaseWorker, task_takes_time
from service.MOptions import MOptions
from service.LongLogicService import LongLogicService

class LongLogicWorker(BaseWorker):

    def __init__(self, frame: wx.Frame, result_event: wx.Event, loop_cnt: int, is_multi_process: bool):
        #temps de traitement
        self.loop_cnt = loop_cnt
        #Exécuter ou non plusieurs processus
        self.is_multi_process = is_multi_process

        super().__init__(frame, result_event)

    @task_takes_time
    def thread_event(self):
        start = time.time()

        #Farce para et options
        # max_La valeur maximale des workers est Python3.Basé sur la valeur par défaut de 8
        options = MOptions(self.frame.version_name, self.frame.logging_level, self.loop_cnt, max_workers=(1 if not self.is_multi_process else min(32, os.cpu_count() + 4)))
        
        #Exécution du service logique
        LongLogicService(options).execute()

        #temps écoulé
        self.elapsed_time = time.time() - start

    def post_event(self):
        #Appelez et exécutez l'événement une fois le traitement logique terminé [Point.11】
        wx.PostEvent(self.frame, self.result_event(result=self.result and not self.is_killed, elapsed_time=self.elapsed_time))

LongLogicWorker hérite de BaseWorker.

BaseWorker.py


# -*- coding: utf-8 -*-
#
import wx
import wx.xrc
from abc import ABCMeta, abstractmethod
from threading import Thread
from functools import wraps
import time
import threading

from utils.MLogger import MLogger # noqa

logger = MLogger(__name__)


# https://wiki.wxpython.org/LongRunningTasks
# https://teratail.com/questions/158458
# http://nobunaga.hatenablog.jp/entry/2016/06/03/204450
class BaseWorker(metaclass=ABCMeta):

    """Worker Thread Class."""
    def __init__(self, frame, result_event):
        """Init Worker Thread Class."""
        #Interface graphique parent
        self.frame = frame
        #temps écoulé
        self.elapsed_time = 0
        #Événement d'appel après la fin du thread
        self.result_event = result_event
        #Jauge de progression
        self.gauge_ctrl = frame.gauge_ctrl
        #Traitement réussi
        self.result = True
        #Avec ou sans commande d'arrêt
        self.is_killed = False

    #Début du fil
    def start(self):
        self.run()

    #Arrêt de fil
    def stop(self):
        #Activer l'interruption FLG
        self.is_killed = True

    def run(self):
        #Exécution de thread
        self.thread_event()

        #Exécution post-traitement
        self.post_event()
    
    def post_event(self):
        wx.PostEvent(self.frame, self.result_event(result=self.result))
    
    @abstractmethod
    def thread_event(self):
        pass


# https://doloopwhile.hatenablog.com/entry/20090627/1275175850
class SimpleThread(Thread):
    """Un thread qui exécute simplement un objet appelable (comme une fonction)"""
    def __init__(self, base_worker, acallable):
        #Traitement dans un autre thread
        self.base_worker = base_worker
        #Méthode pour se déplacer dans le décorateur de fonction
        self.acallable = acallable
        #Résultat du décorateur de fonction
        self._result = None
        #FLG suspendu=Initialiser à l'état OFF
        super(SimpleThread, self).__init__(name="simple_thread", kwargs={"is_killed": False})
    
    def run(self):
        self._result = self.acallable(self.base_worker)
    
    def result(self):
        return self._result


def task_takes_time(acallable):
    """
Décorateur de fonction [Point.10】
Lors de l'exécution du traitement d'origine d'un appelable dans un autre thread
Mettre à jour la fenêtre wx.Continuez d'appeler YieldIfNeeded
    """
    @wraps(acallable)
    def f(base_worker):
        t = SimpleThread(base_worker, acallable)
        #Le démon tue les enfants quand les parents meurent
        t.daemon = True
        t.start()
        #Continuez à mettre à jour les dessins de fenêtre pendant toute la durée de vie du fil
        while t.is_alive():
            #Faites tourner la jauge de progression
            base_worker.gauge_ctrl.Pulse()
            #Actualisez la fenêtre si nécessaire
            wx.YieldIfNeeded()
            #Attendre un peu
            time.sleep(0.01)

            if base_worker.is_killed:
                # 【Point.23] Si l'appelant émet une commande d'arrêt, vous(GUI)Commande de terminaison à tous les threads sauf
                for th in threading.enumerate():
                    if th.ident != threading.current_thread().ident and "_kwargs" in dir(th):
                        th._kwargs["is_killed"] = True
                break
        
        return t.result()
    return f

Point 10: Exécutez un thread GUI et un autre thread avec le décorateur de fonction

C'est une histoire que j'ai déjà reçue du site de référence, mais je vais continuer à mettre à jour le dessin du fil GUI tout en exécutant un autre fil avec la fonction décorateur. Dans SimpleThread, exécutez ʻacallable. À ce stade, la raison pour laquelle «BaseWorker» est maintenu est de passer le drapeau de suspension. Tant que SimpleThread` est actif, le thread GUI met uniquement à jour le dessin et accepte les interruptions. (Cette zone sera affichée plus tard)

Point 11: Appeler et exécuter l'événement une fois le traitement logique terminé

À l'avance, lorsque le worker a été initialisé, l'événement d'appel après le traitement a été passé, alors exécutez-le avec wx.PostEvent. Cela vous ramènera au traitement dans l'interface graphique.

Point 12: Faire connaître explicitement la fin de la logique

Tous les utilisateurs ne sont pas bloqués sur le PC tant que la logique n'est pas terminée, donc lors du dimensionnement, nous émettons un son INFO afin qu'il soit facile à comprendre quand il est terminé. En fonction de l'environnement Windows et d'autres environnements tels que Linux, une erreur peut se produire, il semble donc préférable de la faire sonner uniquement lorsqu'elle peut être émise avec try-except. En passant, si vous donnez le temps écoulé, le sentiment de combien de temps cela a pris sera quantifié, donc je pense que c'est facile à comprendre.

LongLogicService.py


# -*- coding: utf-8 -*-
#

import logging
from time import sleep
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor

from service.MOptions import MOptions
from utils.MException import MLogicException, MKilledException
from utils.MLogger import MLogger # noqa

logger = MLogger(__name__)


class LongLogicService():
    def __init__(self, options: MOptions):
        self.options = options

    def execute(self):
        logging.basicConfig(level=self.options.logging_level, format="%(message)s [%(module_name)s]")

        # 【Point.13] essayez le tout-Entourer sauf et afficher le contenu de l'erreur
        try:
            #Il est normal de mettre la logique normalement
            self.execute_inner(-1)
            
            logger.info("--------------")

            #Il est acceptable de distribuer avec des tâches parallèles
            futures = []
            # 【Point.14] Donnez un nom à la tâche parallèle
            with ThreadPoolExecutor(thread_name_prefix="long_logic", max_workers=self.options.max_workers) as executor:
                for n in range(self.options.loop_cnt):
                    futures.append(executor.submit(self.execute_inner, n))

            #【Point.15] Les tâches parallèles attendent d'être terminées après avoir été émises dans un lot
            concurrent.futures.wait(futures, timeout=None, return_when=concurrent.futures.FIRST_EXCEPTION)

            for f in futures:
                if not f.result():
                    return False

            logger.info("Fin du traitement logique long", decoration=MLogger.DECORATION_BOX, title="Fin logique")

            return True
        except MKilledException:
            #En cas de résiliation par option de suspension, seul le résultat est renvoyé tel quel
            return False
        except MLogicException as se:
            #Erreur de données incomplète
            logger.error("Il s'est terminé avec des données qui ne peuvent pas être traitées.\n\n%s", se.message, decoration=MLogger.DECORATION_BOX)
            return False
        except Exception as e:
            #Autres erreurs
            logger.critical("Le processus s'est terminé par une erreur involontaire.", e, decoration=MLogger.DECORATION_BOX)
            return False
        finally:
            logging.shutdown()

    def execute_inner(self, n: int):
        for m in range(5):
            logger.info("n: %s - m: %s", n, m)
            sleep(1)
        
        return True

Point 13: Dans le thread logique, entourez le tout avec try-except

Étant donné que les threads sont séparés, si vous n'excluez pas correctement l'erreur, vous risquez de vous retrouver soudainement sans savoir de quoi il s'agit. C'est difficile à chasser par la suite, alors excluons-le et enregistrons-le.

Point 14: Donner un nom à la tâche parallèle

Lors de l'exécution de tâches parallèles, il est plus facile de déboguer si vous ajoutez un préfixe afin que vous puissiez facilement comprendre quel processus est le problème.

Point 15: Les tâches parallèles attendent d'être terminées après l'émission du lot

Les tâches parallèles sont d'abord émises avec ʻexecutor.submitpuis attendues avecconcurrent.futures.waitjusqu'à ce que tout le traitement soit terminé. A ce moment, l'optionconcurrent.futures.FIRST_EXCEPTION` est ajoutée afin que le traitement soit interrompu si une exception se produit.

MLogger.py


# -*- coding: utf-8 -*-
#
from datetime import datetime
import logging
import traceback
import threading

from utils.MException import MKilledException


# 【Point.16] Implémentez votre propre enregistreur
class MLogger():

    DECORATION_IN_BOX = "in_box"
    DECORATION_BOX = "box"
    DECORATION_LINE = "line"
    DEFAULT_FORMAT = "%(message)s [%(funcName)s][P-%(process)s](%(asctime)s)"

    DEBUG_FULL = 2
    TEST = 5
    TIMER = 12
    FULL = 15
    INFO_DEBUG = 22
    DEBUG = logging.DEBUG
    INFO = logging.INFO
    WARNING = logging.WARNING
    ERROR = logging.ERROR
    CRITICAL = logging.CRITICAL
    
    total_level = logging.INFO
    is_file = False
    outout_datetime = ""
    
    logger = None

    #Initialisation
    # 【Point.17] Être capable de définir le niveau de sortie minimum pour chaque module
    def __init__(self, module_name, level=logging.INFO):
        self.module_name = module_name
        self.default_level = level

        #Enregistreur
        self.logger = logging.getLogger("PyLogicSample").getChild(self.module_name)

        #Gestionnaire de sortie standard
        sh = logging.StreamHandler()
        sh.setLevel(level)
        self.logger.addHandler(sh)

    # 【Point.18] Préparez une méthode de journalisation avec un niveau inférieur à celui du débogage
    def test(self, msg, *args, **kwargs):
        if not kwargs:
            kwargs = {}

        kwargs["level"] = self.TEST
        kwargs["time"] = True
        self.print_logger(msg, *args, **kwargs)
    
    def debug(self, msg, *args, **kwargs):
        if not kwargs:
            kwargs = {}
            
        kwargs["level"] = logging.DEBUG
        kwargs["time"] = True
        self.print_logger(msg, *args, **kwargs)
    
    def info(self, msg, *args, **kwargs):
        if not kwargs:
            kwargs = {}
            
        kwargs["level"] = logging.INFO
        self.print_logger(msg, *args, **kwargs)

    def warning(self, msg, *args, **kwargs):
        if not kwargs:
            kwargs = {}
            
        kwargs["level"] = logging.WARNING
        self.print_logger(msg, *args, **kwargs)

    def error(self, msg, *args, **kwargs):
        if not kwargs:
            kwargs = {}
            
        kwargs["level"] = logging.ERROR
        self.print_logger(msg, *args, **kwargs)

    def critical(self, msg, *args, **kwargs):
        if not kwargs:
            kwargs = {}
            
        kwargs["level"] = logging.CRITICAL
        self.print_logger(msg, *args, **kwargs)

    #Sortie réelle
    def print_logger(self, msg, *args, **kwargs):

        #【Point.22] Suspend FLG sur le thread en cours d’exécution=Si ON est réglé, une erreur d'interruption se produira.
        if "is_killed" in threading.current_thread()._kwargs and threading.current_thread()._kwargs["is_killed"]:
            #Si une commande d'arrêt est émise, une erreur
            raise MKilledException()

        target_level = kwargs.pop("level", logging.INFO)
        #Sortie uniquement si les niveaux de journalisation de l'application et du module sont atteints
        if self.total_level <= target_level and self.default_level <= target_level:

            if self.is_file:
                for f in self.logger.handlers:
                    if isinstance(f, logging.FileHandler):
                        #Supprimer tous les gestionnaires de fichiers existants
                        self.logger.removeHandler(f)

                #S'il y a une sortie de fichier, association de gestionnaire
                #Gestionnaire de sortie de fichier
                fh = logging.FileHandler("log/PyLogic_{0}.log".format(self.outout_datetime))
                fh.setLevel(self.default_level)
                fh.setFormatter(logging.Formatter(self.DEFAULT_FORMAT))
                self.logger.addHandler(fh)

            #Ajouté au nom du module de sortie
            extra_args = {}
            extra_args["module_name"] = self.module_name

            #Génération d'enregistrement de journal
            if args and isinstance(args[0], Exception):
                # 【Point.19] Lorsqu'une exception est reçue, une trace de pile est émise.
                log_record = self.logger.makeRecord('name', target_level, "(unknown file)", 0, "{0}\n\n{1}".format(msg, traceback.format_exc()), None, None, self.module_name)
            else:
                log_record = self.logger.makeRecord('name', target_level, "(unknown file)", 0, msg, args, None, self.module_name)
            
            target_decoration = kwargs.pop("decoration", None)
            title = kwargs.pop("title", None)

            print_msg = "{message}".format(message=log_record.getMessage())
            
            # 【Point.20] Décorez les messages du journal avec des paramètres
            if target_decoration:
                if target_decoration == MLogger.DECORATION_BOX:
                    output_msg = self.create_box_message(print_msg, target_level, title)
                elif target_decoration == MLogger.DECORATION_LINE:
                    output_msg = self.create_line_message(print_msg, target_level, title)
                elif target_decoration == MLogger.DECORATION_IN_BOX:
                    output_msg = self.create_in_box_message(print_msg, target_level, title)
                else:
                    output_msg = self.create_simple_message(print_msg, target_level, title)
            else:
                output_msg = self.create_simple_message(print_msg, target_level, title)
        
            #production
            try:
                if self.is_file:
                    #S'il y a une sortie de fichier, régénérez l'enregistrement et sortez à la fois la console et l'interface graphique
                    log_record = self.logger.makeRecord('name', target_level, "(unknown file)", 0, output_msg, None, None, self.module_name)
                    self.logger.handle(log_record)
                else:
                    # 【Point.21] Le fil logique est sorti séparément pour l'impression et l'enregistreur
                    print(output_msg)
                    self.logger.handle(log_record)
            except Exception as e:
                raise e
            
    def create_box_message(self, msg, level, title=None):
        msg_block = []
        msg_block.append("■■■■■■■■■■■■■■■■■")

        if level == logging.CRITICAL:
            msg_block.append("■ **CRITICAL** ")

        if level == logging.ERROR:
            msg_block.append("■ **ERROR** ")

        if level == logging.WARNING:
            msg_block.append("■ **WARNING** ")

        if level <= logging.INFO and title:
            msg_block.append("■ **{0}** ".format(title))

        for msg_line in msg.split("\n"):
            msg_block.append("■ {0}".format(msg_line))

        msg_block.append("■■■■■■■■■■■■■■■■■")

        return "\n".join(msg_block)

    def create_line_message(self, msg, level, title=None):
        msg_block = []

        for msg_line in msg.split("\n"):
            msg_block.append("■■ {0} --------------------".format(msg_line))

        return "\n".join(msg_block)

    def create_in_box_message(self, msg, level, title=None):
        msg_block = []

        for msg_line in msg.split("\n"):
            msg_block.append("■ {0}".format(msg_line))

        return "\n".join(msg_block)

    def create_simple_message(self, msg, level, title=None):
        msg_block = []
        
        for msg_line in msg.split("\n"):
            # msg_block.append("[{0}] {1}".format(logging.getLevelName(level)[0], msg_line))
            msg_block.append(msg_line)
        
        return "\n".join(msg_block)

    @classmethod
    def initialize(cls, level=logging.INFO, is_file=False):
        # logging.basicConfig(level=level)
        logging.basicConfig(level=level, format=cls.DEFAULT_FORMAT)
        cls.total_level = level
        cls.is_file = is_file
        cls.outout_datetime = "{0:%Y%m%d_%H%M%S}".format(datetime.now())

Point 16: Implémentez votre propre enregistreur

Peut-être que la partie logger est celle que j'ai le plus d'ingéniosité. Il est plus facile pour les utilisateurs de sortir le journal selon un certain format, mais il est très difficile de le définir un par un. Vous pouvez décorer des messages tels que des boîtes et des bordures avec un seul drapeau. Et surtout, l'enregistreur est utilisé pour juger le drapeau d'interruption. (Les détails seront décrits plus tard)

Point 17: Être capable de définir le niveau de sortie minimum pour chaque module

Si vous définissez le niveau de sortie minimum pour chaque module, vous pouvez supprimer le journal de débogage des méthodes utilitaires en particulier. Effacer physiquement le journal de débogage ou le commenter peut être un problème à vérifier. En augmentant ou en abaissant le niveau minimum de chaque module, vous pouvez contrôler le niveau du journal de sortie, ce qui facilitera le débogage.

Point.18: Préparez une méthode de journalisation avec un niveau inférieur à celui du débogage

Bien qu'il soit associé à 17, il est plus facile de supprimer la sortie en préparant une méthode de bas niveau.

Point 19: trace de la pile de sortie lorsque l'exception est reçue

Ceci est principalement utile lorsque vous obtenez une exception non gérée. De plus, étant donné que l'enregistreur est géré à la fois par le thread GUI et le thread logique, il n'est pas nécessaire d'ajuster la sortie à la source du thread logique.

Point 20: Décorez les messages du journal avec des paramètres

Étant donné que le contrôle de la console a été remplacé par un contrôle de texte normal, le blocage des messages est fortement utilisé pour plus de clarté. Le nombre de messages étant variable, il n'a pas été possible d'attribuer une chaîne de caractères fixe, le blocage est donc appelé pour chaque paramètre donné. Je pense qu'il y a un moyen de spécifier la méthode à l'appelant, mais je pense que c'est plus facile à gérer en termes de sens. Même lors de la décoration de texte, la quantité de code sera moindre s'il est géré à un seul endroit de cette manière plutôt que d'être séparé par l'appelant.

Point.21: Exécutez print et logger.handle séparément pendant le traitement de sortie

Pour imprimer sur le contrôle de la console, vous avez besoin de la sortie de print, et pour imprimer dans le flux, vous avez besoin de la sortie de logger.handle. Les deux messages génèrent les mêmes informations et des informations telles que le module de sortie et l'heure de sortie sont ajoutées à la sortie du flux pour faciliter le suivi.

Point.22: Une erreur de suspension se produit si suspend FLG = ON est défini pour le thread en cours d'exécution.

C'est là que j'étais le plus inquiet ... Voici quelques-unes des pratiques de type thread de Python. --Ne tuez pas les fils de l'extérieur

Référence: https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread

Même si je regarde les paramètres d'interruption en interne, je ne veux pas les voir pour chaque processus logique, et je ne veux pas transporter les paramètres ... Alors, n'est-ce pas l'enregistreur qui passe toujours par une logique? Donc, si vous pouvez le voir sur l'enregistreur et que vous pouvez le changer à partir du thread GUI, il s'agit du "thread actuel" ... En disant cela, c'est devenu comme ça.

Je ne sais pas si c'est une bonne méthode, mais je l'aime parce que le processus logique ne se soucie pas des interruptions.

Point 23: Lorsque l'interruption est indiquée du côté de l'interface graphique, activez toutes les interruptions de thread FLG sauf GUI

Le FLG de suspension est défini dans le décorateur de fonctions BaseWorker. Suspendre tous les threads vivants autres que les threads GUI En activant FLG, vous pouvez voir l'interruption en regardant n'importe quel thread. Maintenant, lorsque vous essayez de sortir le journal, une erreur se produit et vous serez renvoyé à l'interface graphique.

Voie de terminaison normale

image.png

Route d'extrémité suspendue

image.png

3.2. Débogage à partir de VSCode

Maintenant que nous avons créé l'environnement, faisons-le fonctionner à partir de VS Code.

Dans le champ Python Path de l'espace de travail, spécifiez le chemin complet de ʻAnaconda> envs> Environnement de développement> python.exe`.

image.png

Utilisez launch pour spécifier l'exécution de l'exe.

{
	"folders": [
		{
			"path": "src"
		}
	],
	"settings": {
		"python.pythonPath": "C:\\Development\\Anaconda3\\envs\\pytest_env\\python.exe"
	},
	"launch": {
		"version": "0.2.0",
		"configurations": [
			{
				"name": "Python: debug",
				"type": "python",
				"request": "launch",
				"program": "${workspaceFolder}/executor.py",
				"console": "integratedTerminal",
				"pythonPath": "${command:python.interpreterPath}",
				"stopOnEntry": false,
				"args": [
					// "--verbose", "1",                       //le minimum
					// "--verbose", "2",                       // DEBUG_FULL
					// "--verbose", "15",                   // FULL
					"--verbose", "10",                    // TEST
					// "--verbose", "20",                    // INFO
				]
			}
		]
	}
}

Vous pouvez maintenant lancer l'interface graphique à partir de VS Code.

3.3. Créer un exe

J'ai mis en place beaucoup de code, mais à la fin je dois en faire un exe. Voici donc les lots et les fichiers de configuration qui créent PythonExe.

pyinstaller64.bat


@echo off
rem --- 
rem ---Générer l'exe
rem --- 

rem ---Changer le répertoire actuel en destination d'exécution
cd /d %~dp0

cls

rem ---Après être passé à l'environnement de version, exécutez pyinstaller
rem ---Revenir à l'environnement de développement une fois terminé
activate pytest_release && pyinstaller --clean pytest64.spec && activate pytest_env

pytest64.spec


# -*- coding: utf-8 -*-
# -*- mode: python -*-
#Exemple de version 64 bits de PythonExe

block_cipher = None


a = Analysis(['src\\executor.py'],
             pathex=[],
             binaries=[],
             datas=[],
             #Importation de bibliothèque masquée
             hiddenimports=['wx._adv', 'wx._html', 'pkg_resources.py2_warn'],
             hookspath=[],
             runtime_hooks=[],
             #Bibliothèques à exclure
             excludes=['mkl','libopenblas'],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          #nom de l'application
          name='PythonExeSample.exe',
          #Afficher ou non le journal de débogage lors de la création d'un exe
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          #Afficher ou non la console
          console=False )

Si vous voulez vous en tenir à Exe, exécutez simplement le lot, passez à l'environnement de publication, exécutez pyinstaller, puis revenez à l'environnement de développement. Vous n'avez plus à vous soucier d'ajouter accidentellement des bibliothèques supplémentaires à votre environnement de publication.

Le fichier de spécification est le fichier de configuration de pyinstaller, mais c'est le paramètre ajouté par la ligne de commentaire.

hiddenimports

pyinstaller regroupe essentiellement automatiquement les bibliothèques appelées dans le code, mais certaines bibliothèques ne peuvent pas être incluses telles quelles. Ce sont les "importations cachées" qui l'importent explicitement. La seule façon de trouver cela est de changer debug = False en bas à True et de rechercher la partie qui cause l'erreur ... Je pense que c'est une tâche très simple.

excludes

Au contraire, si vous voulez l'exclure parce que la taille du fichier devient grande s'il est regroupé, spécifiez-le avec ʻexcludes. Dans ce cas, mkl et libopenblas` sont exclus en se référant à https://www.reddit.com/r/pygame/comments/aelypb/why_is_my_pyinstaller_executable_180_mb_large/. L'exe terminé est d'environ 30M. (Même si elle est exclue, cette taille ...

3.4. Ce que vous voulez ajouter

--À propos des icônes --À propos de la mémoire externe (json)

Je peux l'ajouter lorsque mon énergie récupère. Comment allez-vous ici avec le dimensionnement VMD? Les questions sont également les bienvenues.

4. Code source

Tout le code ci-dessus peut être trouvé à https://github.com/miu200521358/PythonExeSample. Si vous êtes intéressé, veuillez fourchette et jetez un œil au contenu.

Recommended Posts

Si vous souhaitez créer une application Windows (exe) qui peut être utilisée maintenant en utilisant uniquement Python
Si vous souhaitez créer une application TODO (distribuée) maintenant en utilisant uniquement Python
[Python3] Code qui peut être utilisé lorsque vous souhaitez découper une image dans une taille spécifique
[Python3] Code qui peut être utilisé lorsque vous souhaitez redimensionner des images dossier par dossier
Si vous souhaitez créer une application TODO (distribuée) en utilisant uniquement Python-Extension 1
Je souhaite créer une application Web en utilisant React et Python flask
Je souhaite créer une file d'attente prioritaire pouvant être mise à jour avec Python (2.7)
Comment installer la bibliothèque Python qui peut être utilisée par les sociétés pharmaceutiques
Si vous voulez créer un bot discord avec python, utilisons un framework
[Python3] Code qui peut être utilisé lorsque vous souhaitez modifier l'extension d'une image à la fois
Scripts pouvant être utilisés lors de l'utilisation de Bottle en Python
[Python] Créez un graphique qui peut être déplacé avec Plotly
Si vous souhaitez affecter une exportation csv à une variable en python
J'ai essayé de créer une application todo en utilisant une bouteille avec python
Vérifiez si vous pouvez vous connecter à un port TCP en Python
J'ai créé un modèle de projet Python générique
Comment créer un bot Janken qui peut être facilement déplacé (commentaire)
[Python] Si vous souhaitez dessiner un diagramme de dispersion de plusieurs clusters
Deux outils de génération de documents que vous souhaitez absolument utiliser si vous écrivez python
Si vous souhaitez afficher la valeur à l'aide des choix du modèle dans le modèle Django
Si "ne peut pas être utilisé lors de la création d'un objet PIE" apparaît dans make
Je veux exécuter et distribuer un programme qui redimensionne les images Python3 + pyinstaller
Commande Linux (édition de base) utilisable à partir d'aujourd'hui si vous connaissez
Je veux faire un jeu avec Python
Si vous souhaitez créer Word Cloud.
Convertir des images du SDK FlyCapture en un formulaire pouvant être utilisé avec openCV
Résumé des méthodes d'analyse de données statistiques utilisant Python qui peuvent être utilisées en entreprise
[Mac] Je souhaite créer un serveur HTTP simple qui exécute CGI avec Python
[Python] Vous pouvez enregistrer un objet dans un fichier en utilisant le module pickle.
[Python] Introduction au scraping WEB | Résumé des méthodes pouvant être utilisées avec webdriver
J'ai essayé de faire une application mémo qui peut être pomodoro, mais un enregistrement de réflexion
Un mécanisme pour appeler des méthodes Ruby à partir de Python qui peut être fait en 200 lignes
(Python) Essayez de développer une application Web en utilisant Django
Notes sur les connaissances Python utilisables avec AtCoder
Comment créer un package Python à l'aide de VS Code
[Python] Je veux faire d'une liste imbriquée un taple
Seuls les tableaux de taille 1 peuvent être convertis en scalaires Python
J'ai créé un outil pour générer automatiquement un diagramme de transition d'état pouvant être utilisé à la fois pour le développement Web et le développement d'applications
Comment configurer un serveur SMTP simple qui peut être testé localement en Python
Je veux faire un changeur de voix en utilisant Python et SPTK en référence à un site célèbre
[Python] Un programme pour trouver le nombre de pommes et d'oranges qui peuvent être récoltées
[Django] Mémorandum lorsque vous souhaitez communiquer de manière asynchrone [Python3]
[Python] Si vous souhaitez soudainement créer un formulaire de demande
Comment transloquer un tableau à deux dimensions en utilisant uniquement python [Note]
J'ai fait un chronomètre en utilisant tkinter avec python
Je veux ajouter un joli complément à input () en python
Lorsque vous souhaitez lancer une commande UNIX sur Python
Faisons un diagramme sur lequel on peut cliquer avec IPython
Comprendre les probabilités et les statistiques qui peuvent être utilisées pour la gestion des progrès avec un programme python
[Python] J'ai essayé de créer un programme simple qui fonctionne sur la ligne de commande en utilisant argparse
・ <Slack> Ecrire une fonction pour notifier Slack afin qu'elle puisse être citée à tout moment (Python)
Lorsque vous souhaitez remplacer plusieurs caractères dans une chaîne de caractères sans utiliser d'expressions régulières dans la série python3
Si vous souhaitez devenir data scientist, commencez par Kaggle
Jusqu'à ce que la géométrie de la torche ne puisse être utilisée qu'avec le processeur Windows (ou Mac)
N'écrivez pas Python si vous voulez l'accélérer avec Python
Je souhaite utiliser un caractère générique que je souhaite décortiquer avec Python remove
J'ai essayé de créer un système qui ne récupère que les tweets supprimés
Que faire si vous obtenez moins zéro en Python
J'ai essayé de créer une expression régulière de "montant" en utilisant Python