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 (exe | code)" 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 ...)
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.
Après DL, suivez les étapes pour installer.
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.
De même, créez un environnement de version,
conda create -n pytest_release pip python=3.7
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.
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
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.
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.
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.
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
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é.
print
sur le contrôle de la consoleprint
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.
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()
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.
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).
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
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)
À 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.
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
É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.
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.
Les tâches parallèles sont d'abord émises avec ʻexecutor.submitpuis attendues avec
concurrent.futures.waitjusqu'à ce que tout le traitement soit terminé. A ce moment, l'option
concurrent.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())
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)
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.
Bien qu'il soit associé à 17, il est plus facile de supprimer la sortie en préparant une méthode de bas niveau.
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.
É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.
print
et logger.handle
séparément pendant le traitement de sortiePour 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.
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.
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
Route d'extrémité suspendue
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`.
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.
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 ...
--À 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.
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.