2018 a été un choc pour moi. J'avais l'habitude de jouer avec Kivy kivy de GUI Framekwork, mais j'ai trouvé que l'utilisation d'un générateur rend le code moche plein de fonctions de rappel étonnamment facile à lire. Après avoir essayé diverses choses, j'ai fini par comprendre le traitement asynchrone par async / await, qui était une magie que je ne connaissais pas jusque-là, et j'ai pu créer une petite bibliothèque de traitement asynchrone. Cet article est nouveau
――Le processus jusqu'à ce que je réalise la merveille du générateur et de la coroutine native qui en est née
Je veux le préciser.
(Pour réduire la quantité de texte, le générateur est abrégé en gen et la coroutine est abrégée en coro.)
Je pense que de nombreux livres d'introduction présentent la gen comme un outil de production de valeur. (Le rendement est décrit comme «donner» pour éviter toute confusion avec «retour» qui signifie «retour»)
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, b+a
for i in fibonacci():
print(i, end=' ')
import time; time.sleep(.1)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 ...
Étant donné le nom de «générateur», c'était peut-être cet objectif à l'origine. En fait, jusqu'en 2018, je n'ai vu que ce genre de chose, et je pense qu'il était difficile pour moi de sortir de ce concept car j'avais l'habitude de récupérer la valeur avec for-in
comme ci-dessus.
Certains livres d'introduction incluent également un exemple d'utilisation de send ()
pour récupérer une valeur de gen.
gen = fibonacci()
print(gen.send(None))
print(gen.send(None))
print('Une petite pause')
import time;time.sleep(1)
print('Fin de pause')
print(gen.send(None))
print(gen.send(None))
0
1
Une petite pause
Fin de pause
1
2
Je pense que cela donne un aperçu du "pouvoir caché" de gen, mais je ne l'ai pas remarqué à ce stade non plus. Mais qu'en est-il du prochain exemple?
def sub_task():
print('sub:Processus 1')
yield
print('sub:Processus 2')
yield
print('sub:Processus 3')
def main_task():
gen = sub_task()
try:
print('main:Processus 1')
gen.send(None)
print('main:Processus 2')
gen.send(None)
print('main:Processus 3')
gen.send(None)
except StopIteration:
pass
main_task()
main:Processus 1
sub:Processus 1
main:Processus 2
sub:Processus 2
main:Processus 3
sub:Processus 3
Vous pouvez voir que les deux tâches progressent petit à petit avec gen.send ()
et yield
comme points de commutation. N'est-ce pas ce ** traitement parallèle ** lui-même!
C'était le "pouvoir caché" de gen. gen est
--Il peut être mis en pause ... (S'arrête au rendement et commence à se déplacer avec gen.send ()
)
gen.send ()
du côté utilisateur)C'était comme une fonction, et en raison de sa fonctionnalité stoptable, ** le traitement parallèle pouvait être effectué sans compter sur le multi threading
**.
Cela m'a donné de l'espoir quand je souffrais d'un code laid plein de fonctions de rappel. Parce que, par exemple, lorsque vous souhaitez faire ce qui suit avec Kivy
def some_task():
print('Processus 1')
Attendez que le bouton soit enfoncé
print('Processus 2')
Attendez 1 seconde
print('Processus 3')
Le code réel est
from kivy.clock import Clock
def some_task(button):
print('Processus 1')
def callback2(button):
button.unbind(on_press=callback2)
print('Processus 2')
Clock.schedule_once(callback3, 1)
def callback3(__):
print('Processus 3')
button.bind(on_press=callback2)
Cela devient laid qui ne se voit pas. Vous devez attendre quelque chose
=>Vous devez arrêter le traitement jusqu'à ce que quelque chose se passe
=> Le traitement suivant doit être séparé en une autre fonction
. Mais je veux que vous vous souveniez du sub_task ()
qui est sorti plus tôt.
def sub_task():
print('sub:Processus 1')
yield
print('sub:Processus 2')
yield
print('sub:Processus 3')
De même, la fonction de rappel n'apparaît nulle part même si elle s'est arrêtée au milieu. J'ai donc commencé à me demander si gen pouvait être utilisé pour éliminer la fonction de rappel dans Kivy.
Je vais réfléchir à la méthode à partir de maintenant, mais je voudrais d'abord mentionner que la branche maître actuelle de Kivy peut déjà effectuer une programmation asynchrone à grande échelle en utilisant [asyncio] asyncio_doc et [trio] trio_doc. Donc, ce que nous faisons ici, c'est [réinvention des roues] [roue]. Cependant, à ce moment-là, ce n'était pas le cas, et j'étais simplement intéressé par la génération, alors j'ai choisi de faire quelque chose par moi-même.
J'ai décidé de mettre de côté le bouton une fois et de réaliser une fonction pour arrêter gen pendant ce laps de temps lorsqu'une valeur numérique est envoyée depuis gen. C'est parce que j'ai vu [la vidéo de BeeWare] video_beeware et j'ai pensé que c'était cool de faire une telle chose.
def some_task():
print('Processus 1')
yield 2 #Attendez 2 secondes
print('Processus 2')
yield 1 #Attendez 1 seconde
print('Processus 3')
Considérez comment faire fonctionner la génération ci-dessus comme prévu. Avec les connaissances jusqu'à présent
Clock.schedule_once ()
pour réserver le processus que vous voulez faire après un certain temps sur Kivy.
--Vous devez appeler gen.send ()
pour redémarrer genJe le sais. Ensuite, "Pourquoi ne passons-nous pas la fonction de redémarrage de gen à Clock.schedule_once ()
? "
from kivy.clock import Clock
from kivy.app import App
from kivy.uix.widget import Widget
def start_gen(gen):
def step_gen(dt):
try:
Clock.schedule_once(step_gen, gen.send(None)) # C
except StopIteration:
pass
step_gen(None) # B
def some_task():
print('Processus 1')
yield 1 # D
print('Processus 2')
yield 2 # E
print('Processus 3')
class SampleApp(App):
def build(self):
return Widget()
def on_start(self):
start_gen(some_task()) # A
if __name__ == '__main__':
SampleApp().run()
C'était la bonne réponse. Ce code fonctionne comme suit.
start_gen ()
(ligne A)start_gen ()
appelle immédiatementstep_gen ()
(ligne B)step_gen ()
appellegen.send ()
, donc gen commence à fonctionner (ligne C)gen.send (None)
est 1, step_gen ()
se réserve d'être appelé à nouveau après 1 seconde (ligne C).step_gen ()
est appelé et gen.send ()
est appelé, ainsi gen commence à se déplacer à partir de la position où il s'est arrêté la dernière fois. (Ligne C)C'était choquant de pouvoir attendre un moment sans utiliser la fonction de rappel en préparant une fonction avec seulement 7 lignes (start_gen ()
). Motivé, je continuerai à améliorer cela.
Le temps réel écoulé est passé à la fonction de rappel passée à Clock.schedule_once ()
. Comme c'est un gros problème, je l'ai fait pour que le côté some_task ()
puisse le recevoir. Tout ce que vous avez à faire est de changer la partie gen.send (None)
de start_gen ()
en gen.send (dt)
. Maintenant, le côté some_task ()
peut obtenir le temps réel écoulé comme suit (code entier).
def some_task():
print('Processus 1')
s = yield 1
print(f"Quand j'ai demandé un arrêt pendant 1 seconde, il{s:.03f}Arrêté une seconde")
print('Processus 2')
s = yield 2
print(f"Quand j'ai demandé un arrêt pendant 2 secondes, il{s:.03f}Arrêté une seconde")
print('Processus 3')
Processus 1
Quand j'ai demandé un arrêt pendant 1 seconde, c'était en fait 1.089 secondes arrêtées
Processus 2
Quand j'ai demandé un arrêt pendant 2 secondes, c'était en fait 2.Arrêté pendant 003 secondes
Processus 3
Ensuite, attend l'événement, idéalement si le côté gen écrit comme suit.
def some_task(button):
print('Processus 1')
yield event(button, 'on_press') #Attendez que le bouton soit enfoncé
print('Processus 2')
Dans le cas d'un événement, c'est un peu compliqué car cela nécessite un travail de
def start_gen(gen):
def step_gen(*args, **kwargs):
try:
gen.send((args, kwargs, ))(step_gen)
except StopIteration:
pass
try:
gen.send(None)(step_gen)
except StopIteration:
pass
def event(ed, name):
bind_id = None
step_gen = None
def bind(step_gen_):
nonlocal bind_id, step_gen
bind_id = ed.fbind(name, callback) #Associer la fonction de rappel
assert bind_id > 0 #Vérifier si la liaison a réussi
step_gen = step_gen_
def callback(*args, **kwargs):
ed.unbind_uid(name, bind_id) #Résolvez la fonction de rappel
step_gen(*args, **kwargs) #Reprendre gen
return bind
La grande différence avec l'arrêt temporel est que tout le traitement lié à l'événement peut être caché dans ʻevent () . Grâce à cela,
start_gen () ne dépend pas du tout de kivy, et c'est aussi simple que de passer
step_gen` à l'appelable envoyé depuis gen.
Je pense que la conception ci-dessus est très bonne, donc j'ai supprimé le traitement lié à kivy de start_gen ()
et je l'ai caché dans une autre fonction, après l'événement, attendez l'arrêt du temps.
def sleep(duration):
return lambda step_gen: Clock.schedule_once(step_gen, duration)
Vous pouvez maintenant mélanger sleep ()
et ʻevent () `.
def some_task(button):
yield event(button, 'on_press') #Attendez que le bouton soit enfoncé
button.text = 'Pressed'
yield sleep(1) #Attendez 1 seconde
button.text = 'Bye'
La façon dont gen reprend dépend entièrement de l'appelable envoyé par gen, donc si vous envoyez quelque chose comme ce qui suit, par exemple
def sleep_forever():
return lambda step_gen: None
def some_task():
yield sleep_forever() #Attends pour toujours
Il est également possible de ne pas redémarrer.
Afin de confirmer sa polyvalence, j'ai également traité de choses qui n'ont rien à voir avec Kivy, le fil.
def thread(func, *args, **kwargs):
from threading import Thread
return_value = None
is_finished = False
def wrapper(*args, **kwargs):
nonlocal return_value, is_finished
return_value = func(*args, **kwargs)
is_finished = True
Thread(target=wrapper, args=args, kwargs=kwargs).start()
while not is_finished:
yield sleep(3)
return return_value
C'est devenu une façon terne de regarder autour de vous pour voir si cela se termine régulièrement, mais maintenant le côté gen peut attendre la fin en exécutant la fonction passée sur un autre thread.
class SampleApp(App):
def on_start(self):
start_gen(self.some_task())
def some_task(self):
def heavy_task():
import time
for i in range(5):
time.sleep(1)
print(i)
button = self.root
button.text = 'start heavy task'
yield event(button, 'on_press') #Attendez que le bouton soit enfoncé
button.text = 'running...'
yield from thread(heavy_task) #Lourd sur un autre fil_task()Et attendez sa fin
button.text = 'done'
Cela semble bien se passer ici, mais certains problèmes sont apparus. La première est que vous devez utiliser correctement le rendement et le rendement en fonction de ce que vous attendez. (sleep ()
et ʻevent () sont yield,
thread () est yield from). De plus, cela dépend de l'implémentation, et si
threading.Thread avait un mécanisme pour notifier la fin du thread avec la fonction de rappel,
thread () `pourrait également être implémenté afin qu'il puisse attendre avec yield. Ce n'est pas bon que l'utilisation soit différente comme ça, j'ai donc décidé de l'unifier à l'un ou l'autre.
Je pense que la seule option est «rendement de». Parce qu'il est facile de faire ce que vous pouvez attendre pour le rendement de l'attente du rendement, mais pas toujours l'inverse. Par exemple
def some_gen():
yield 1
«1» est
def some_gen():
yield from one()
def one():
yield 1
En faisant cela, vous pouvez attendre à yield from
def some_gen():
yield from another_gen()
def another_gen():
yield 1
yield 4
ʻAnother_gen () ne peut probablement pas attendre avec
yield another_gen () `.
J'ai donc réécrit sleep ()
et ʻevent () `pour attendre le rendement.
def sleep(duration):
return (yield lambda step_coro: Clock.schedule_once(step_gen, duration))
def event(ed, name):
#Abréviation
return (yield bind)
Avec cela, l'utilisateur n'a pas à utiliser correctement le rendement et le rendement.
#Toujours céder de
def some_task():
yield from sleep(2)
yield from event(button, 'on_press')
yield from thread(heavy_task)
Un autre problème est que le côté gen utilisait à l'origine les arguments passés à la fonction de rappel comme suit.
def some_task():
s = yield 1
print(f"Quand j'ai demandé un arrêt pendant 1 seconde, il{s:.03f}Arrêté une seconde")
Ce qui se passe maintenant est en fait
def some_task():
args, kwargs = yield from sleep(1)
s = args[0]
print(f"Quand j'ai demandé un arrêt pendant 1 seconde, il{s:.03f}Arrêté une seconde")
Il est difficile d'obtenir la valeur requise. C'est parce que l'argument formel est def step_gen (* args, ** kwargs):
pour que step_gen ()
puisse recevoir n'importe quel argument. Heureusement, cependant, grâce à l'unification à partir de laquelle céder, un tel traitement peut être fait du côté sleep ()
.
def sleep(duration):
args, kwargs = yield lambda step_coro: Clock.schedule_once(step_coro, duration)
return args[0]
Avec cela, le côté utilisateur
def some_task():
s = yield from sleep(1)
print(f"Quand j'ai demandé un arrêt pendant 1 seconde, il{s:.03f}Arrêté une seconde")
Cela suffisait.
Ensuite, j'ai décidé de convertir ce que j'ai fait jusqu'à présent afin qu'il puisse être géré avec la syntaxe async / await introduite dans Python 3.5. C'est parce qu'il a été écrit dans divers documents qu'il s'agit d'un remplacement de gen comme coro. Pour moi, j'ai essayé de faire «attendre» avec un mot plus court et plus facile à lire que «céder de» avec deux mots, mais il semble que cela présente en fait plus d'avantages, alors veuillez vous référer aux détails. Voir [Officiel] pep492.
Premièrement, les fonctions gen qui incluent des rendements sans from, comme sleep ()
et ʻevent () , ont reçu
@ types.coroutine. C'est parce que je ne savais pas comment réécrire la fonction gen contenant l'instruction return comme celle-ci en tant que fonction async. (Si la fonction async a une expression yield et renvoie une valeur dans l'instruction return, une erreur de syntaxe se produira:
SyntaxError: 'return' avec valeur dans le générateur async`)
import types
@types.coroutine
def sleep(duration):
#Abréviation
@types.coroutine
def event(ed, name):
#Abréviation
D'un autre côté, thread ()
et some_task ()
pourraient être réécrits en tant que fonctions asynchrones pures. En particulier
yield from
à ʻawait --
def à ʻasync def
Remplacé.
async def thread(func, *args, **kwargs):
#Abréviation
while not is_finished:
await sleep(3)
#Abréviation
class SampleApp(App):
async def some_task(self):
#Abréviation
await event(button, 'on_press')
button.text = 'running...'
await thread(heavy_task)
button.text = 'done'
Enfin, remplacez la chaîne de caractères «gen» incluse dans l'identificateur par «coro» pour terminer.
C'est la fin de l'édition Kivy. Comme j'ai oublié de le dire, coro peut être exécuté en même temps autant de fois que start_coro (another_task ())
. ʻAttends another_task () si vous voulez attendre la fin,
start_coro (another_task ()) `si vous voulez exécuter en parallèle sans attendre.
Ensuite, j'ai essayé la même chose avec tkinter, mais la procédure était exactement la même que kivy (en passant la fonction de redémarrage de gen / coro comme fonction de rappel), donc ça s'est bien passé.
La différence avec Kivy est que Kivy utilise le seul objet kivy.clock.Clock
, tandis que tkinter utilise la méthode .after ()
de chaque widget. Vous devez donc spécifier quel widget .after ()
appeler en plus de l'heure à laquelle vous souhaitez vous arrêter.
@types.coroutine
def sleep(widget, duration):
yield lambda step_coro: widget.after(duration, step_coro)
Cela signifie que vous devez passer le widget à thread ()
, qui utilise sleep ()
en interne.
async def thread(func, *, watcher):
#Abréviation
while not is_finished:
await sleep(watcher, 3000) #3000ms d'arrêt
return return_value
Vient ensuite l'événement. Avant l'implémentation, ʻunbind () `de tkinter semble avoir un [bogue] tkinter_issue, donc je l'ai modifié comme suit, en me basant sur les informations liées.
def _new_unbind(self, sequence, funcid=None):
if not funcid:
self.tk.call('bind', self._w, sequence, '')
return
func_callbacks = self.tk.call('bind', self._w, sequence, None).split('\n')
new_callbacks = [l for l in func_callbacks if l[6:6 + len(funcid)] != funcid]
self.tk.call('bind', self._w, sequence, '\n'.join(new_callbacks))
self.deletecommand(funcid)
def patch_unbind():
from tkinter import Misc
Misc.unbind = _new_unbind
Il est remplacé par le ʻunbind () modifié en appelant
patch_unbind () . Le problème est de savoir quand l'appeler, mais je pense qu'il vaut mieux ne pas le faire sans permission car il modifie tkinter lui-même. J'ai donc décidé que l'utilisateur m'appelle explicitement. Et l'implémentation de ʻevent ()
@types.coroutine
def event(widget, name):
bind_id = None
step_coro = None
def bind(step_coro_):
nonlocal bind_id, step_coro
bind_id = widget.bind(name, callback, '+')
step_coro = step_coro_
def callback(*args, **kwargs):
widget.unbind(name, bind_id)
step_coro(*args, **kwargs)
return (yield bind)[0][0]
C'est devenu.
#Installez asynctkinter à l'avance
# pip install git+https://github.com/gottadiveintopython/asynctkinter#egg=asynctkinter
from tkinter import Tk, Label
import asynctkinter as at
at.patch_unbind() # unbind()Réparer le bug
def heavy_task():
import time
for i in range(5):
time.sleep(1)
print('heavy task:', i)
root = Tk()
label = Label(root, text='Hello', font=('', 60))
label.pack()
async def some_task(label):
label['text'] = 'start heavy task'
event = await at.event(label, '<Button>') #Attendez que l'étiquette soit appuyée
print(event.x, event.y)
label['text'] = 'running...'
await at.thread(heavy_task, watcher=label) #Lourd sur un autre fil_task()Et attendez sa fin
label['text'] = 'done'
await at.sleep(label, 2000) #Attendez 2 secondes
label['text'] = 'close the window'
at.start(some_task(label))
root.mainloop()
L'idée d'utiliser gen pour le traitement parallèle semble avoir [déjà existé] video_curious_course il y a environ 10 ans, et beaucoup de gens l'ont peut-être su, mais pour moi, c'était une nouvelle connaissance il y a un an ou deux et c'était un choc. J'ai donc créé cet article. Probablement, si le côté bibliothèque implémente la boucle d'événements comme tkinter, utilisez cette méthode, et si le côté utilisateur confie l'implémentation de la boucle d'événements comme pygame, définissez la boucle d'événements sur ʻasyncioou Si vous l'implémentez comme une tâche sur
trio`, vous pouvez fondamentalement introduire async / await à n'importe quoi. Adieu la fonction de rappel laide.
Recommended Posts