蛇使いな彼女BLOG
【第129回】非同期処理とFletアプリのその後
2025.05.16

こんにちは皆様。
開始早々話がそれるんですが、この4月に風邪をこじらせまして・・
で、思ったんですが最近の風邪進化してませんか(@_@;)?
普通の風邪が5類になったのは知っていましたけど、インフルのような喉の痛みと高熱に加えて感染力も上がっているようで、
我が家では、子供から順番に家族全員に症状がでて終息までにちょうど一週間かかりました。
周りに話を聞くとその時期同じような風邪が流行っていたとか。
皆様も健康には気をつけましょう💦
さて、今日のお話は以前作りかけていたFletアプリが徐々に仕上がってきたので、そちらの紹介と、
今回の制作中、asyncの非同期処理を試してみた感想をまとめます。
Flet(My app)
スクレイピング用のアプリケーション
まだ完成ではありませんが、これ以上ウィジェット数を増やすとコントロールが大変なので機能は超絶シンプルにしました。
以前の記事でpubsubによるデータベース情報の受け渡し方法を紹介したかと思いますが、
今回はそれに加え日付情報を受け取ってリクエストを投げています。
色々試した結果、pubsubは複数のイベントから複数の指定したコンテンツに情報を飛ばすのが難しいと判断したため、メインプロジェクトの辞書を介してコンテンツに情報を飛ばすことにしました。
ファイルを分けたりして、これでもかなり省略しているんですが、全体は長い長いコードです(笑)
今回の要点は一番下側navigation_barの”Run”ボタンを押すと、入力した情報をもとにスクレイピングが実行されるのですが、その間asyncio(非同期処理)を試してみたという話です。
main.py
import numpy as np import flet as ft import datetime as dt from package import DB # データベース from package import ft_Table # デーブル生成用ファンクション from package import functions as FC # スクレイピングファンクション import asyncio ♯ 辞書 temporary={ 'data':'None', 'date_range_S':None, 'date_range_E':None, 'result':None, } ♯ メインアプリケーション def main(page: ft.Page): # setting page.title = "Flet MyApp" page.horizontal_alignment = ft.CrossAxisAlignment.CENTER page.bgcolor = ft.Colors.BLUE_GREY_500 page.heght=500 # appbar page.appbar = ft.AppBar( leading=ft.Icon(ft.Icons.FOUNDATION), leading_width=40, title=ft.Text("My app"), center_title=False, actions=[ ft.PopupMenuButton( items=[ ft.PopupMenuItem(text="help"), ft.PopupMenuItem(text="DBupdate"), ft.PopupMenuItem(text="close"), ] ) ] ) # ProgressBar progress=ft.ProgressBar(width=page.window.width, color="amber", bgcolor="#eeeeee") # Notification Notification=ft.SnackBar( ft.Text(value='The data extraction process is complete.',color=ft.Colors.BLACK,), bgcolor=ft.Colors.AMBER_ACCENT_200, duration=3000 ) # function def date_select_S(e): View.controls[1].controls[1].controls[0].additional_info=ft.Text(f"{e.control.value.strftime('%Y-%m-%d')}") temporary['date_range_S']=f"{e.control.value.strftime('%Y-%m-%d')}" page.update() # function def date_select_E(e): View.controls[1].controls[1].controls[1].additional_info=ft.Text(f"{e.control.value.strftime('%Y-%m-%d')}") temporary['date_range_E']=f"{e.control.value.strftime('%Y-%m-%d')}" page.update() async def SELECTED_TAB(select): if select == 0: print(select) if 'None' not in temporary['data'] and temporary['date_range_S'] is not None and temporary['date_range_E'] is not None: inst=FC.data_request(temporary['data'], temporary['date_range_S'], temporary['date_range_E'], ) View.controls[2].content.controls.append(progress) page.update() # Wait until scraping is finished task=asyncio.create_task(inst.exe()) task_result = await task View.controls[2].content.controls.pop() temporary['result']=task_result temporary['columns']=inst.cols page.open(Notification) page.update() elif 'None' in temporary['data'] : dlg_modal=ft.AlertDialog( modal=True, title=ft.Text("No location!"), content=ft.Text("Add an observation point."), actions=[ ft.TextButton("Yes", on_click=lambda _ : page.close(dlg_modal)), ], actions_alignment=ft.MainAxisAlignment.END, ) page.open(dlg_modal) else: dlg_modal=ft.AlertDialog( modal=True, title=ft.Text("Date period is not valid"), content=ft.Text("Enter the start and end dates."), actions=[ ft.TextButton("Yes", on_click=lambda _ : page.close(dlg_modal)), ], actions_alignment=ft.MainAxisAlignment.END, ) page.open(dlg_modal) pass elif select == 1 : print(select) pass # Navigation bar page.navigation_bar = ft.CupertinoNavigationBar( bgcolor=ft.Colors.GREY_800, inactive_color=ft.Colors.BLUE_GREY_100, active_color=ft.Colors.AMBER, on_change=lambda e:asyncio.run(SELECTED_TAB(e.control.selected_index)), destinations=[ ft.NavigationBarDestination(icon=ft.Icons.NOT_STARTED, label="Run"), ft.NavigationBarDestination( icon=ft.Icons.OUTPUT_OUTLINED, label="Output",), ], ) # function def event(e): page.pubsub.send_all(DB.Input_station(station=input_location.value)) page.update() def on_click_button(station_info:DB.Input_station): print(View.controls[1],page.pubsub) temporary['data']=station_info.data new_table=ft_Table.Create_Table(station_info.data) if View.controls[1].controls[0].content.controls != []: View.controls[1].controls[0].content.controls.pop() View.controls[1].controls[0].content.controls.append(new_table) page.update() # content input_location=ft.TextField(border=ft.InputBorder.NONE,filled=True,expand=True,hint_text="location search") event_button=ft.IconButton(icon=ft.Icons.SEARCH,icon_color='black', on_click=event) # Pass event input to the page page.pubsub.subscribe(on_click_button) View=ft.Column( width=page.window.width, alignment=ft.MainAxisAlignment.SPACE_EVENLY, controls=[ #'''<header>''' ft.Container(expand=1, content=ft.Row( controls=[ input_location, event_button, ], alignment=ft.MainAxisAlignment.CENTER, ), ), #'''<body>''' ft.Row( width=page.window.width, controls=[ ft.Container( bgcolor=ft.Colors.BLUE_GREY_100, height=380, width=380, border_radius=10, content=ft.Row(), ), ft.Column( spacing=10, controls=[ ft.CupertinoListTile( padding=5, bgcolor=ft.Colors.BLUE_GREY_500, notched=True, additional_info=ft.Text("None",), bgcolor_activated=ft.Colors.AMBER_ACCENT, leading=ft.Icon(name=ft.CupertinoIcons.PLUS), title=ft.Text("starting point"), subtitle=ft.Text("10 minute intervals",), trailing=ft.Icon(name=ft.CupertinoIcons.ALARM), on_click=lambda e : page.open( ft.DatePicker( first_date=dt.datetime(year=1900, month=1, day=1), last_date=dt.datetime.today().date(), on_change=date_select_S, on_dismiss=date_select_S, ) ), ), ft.CupertinoListTile( padding=5, bgcolor=ft.Colors.BLUE_GREY_500, notched=True, additional_info=ft.Text("None"), bgcolor_activated=ft.Colors.AMBER_ACCENT, leading=ft.Icon(name=ft.CupertinoIcons.PLUS), title=ft.Text("End point"), subtitle=ft.Text("10 minute intervals",), trailing=ft.Icon(name=ft.CupertinoIcons.ALARM), on_click=lambda e : page.open( ft.DatePicker( first_date=dt.datetime(year=1900, month=1, day=1), last_date=dt.datetime.today().date(), on_change=date_select_E, on_dismiss=date_select_E, ), ), ), ], ), ], ), #'''<footer>''' ft.Container(expand=1,height=150, content=ft.Row( [ ], alignment=ft.MainAxisAlignment.END, ), ), ] ) page.add(View) page.update() ft.app(main)
まずおさらいですが、要素間の情報の受け渡しについて、
基本的に ft.Columnやft.Rowのcontrols、ft.Containerのcontentなどは親要素として他のウィジェットを格納できる特徴を持っています。(複数の場合はリストで指定可能)
この例として、変数Viewは最初、鉛直方向に3つの(header/body/footer)3つのエリアを設けていますが、これらがcontrolsにリスト状に格納されているため、
関数date_select_Sの
View.controls[1].controls[1].controls[0].additional_info というのはbody部分のft.CupertinoListTileのadditional_infoを示します。
このようにコントロールを辿ることで、子要素の変更を行うことが可能です。
# function def date_select_S(e): View.controls[1].controls[1].controls[0].additional_info=ft.Text(f"{e.control.value.strftime('%Y-%m-%d')}") temporary['date_range_S']=f"{e.control.value.strftime('%Y-%m-%d')}" page.update()
次に、アプリ内のボタン操作を行ったとき(イベント時)にはon_click、on_change等の引数に関数を指定すればその処理が実行されます。
ただ、このとき発生するイベントはローカルな値なので、他のウィジェットとのデータのやり取りを行いたい場合は、イベントの値を受け取りながら、main部分のft.Pageも同時に扱わないと上手くいかない所が難点です。
例えば、「Starting point」「End point」を押したときに表示されたピッカーから、入力した日付を受け取る場合、構造的には1つの結果に対し2段階の操作でイベント値を取得します。
↓このように、要素が増えるとどうしてもmain部分が肥大化して可読性が下がります😿
def main(page: ft.Page): page.heght=500 page.horizontal_alignment = ft.CrossAxisAlignment.CENTER def func1(e,page): page.add(ft.Text(f"{e.control.value}")) page.update() def func2(page): page.open(ft.DatePicker( first_date=dt.datetime(year=1900, month=1, day=1), on_change=lambda e:func1(e,page) )) page.add(ft.CupertinoListTile(title=ft.Text("starting point"),on_click=lambda e:func2(page)))
更に、イベントの返り値もコントロールを辿らないといけないため、そこそこやりこんでいないと予めhandlerを設定しておく事自体難易度が高いことが判明しました。
色々試した結果、main部分は下手にオブジェクト化せず、公式ドキュメントで推奨している記述が無難だと感じました。
またデータの受け渡しに関してはシンプルに辞書やデータベースを用意し、そこに格納してから別途処理を行うのが良さそうです。
♯ 辞書 temporary={ 'data':'None', 'date_range_S':None, 'date_range_E':None, 'result':None, }
そしていよいよasyncioによる非同期処理ですが、
「Run」ボタンを押すと、アプリケーション側のイベント実行中にスクレイピングが終わるのを待つことになります。
仮にユーザーが指定した日付範囲が長すぎるとき、ここでタイムアウトの処理を追加したいと考え、asyncio.create_taskやasyncio.wait_forによるタスクのスケジューリングから、後々調整しようと思っていたのですが…
(※試しに短期間で実行してみると、スクレイピング処理自体はasyncioを使おうが使わまいが無事完了していることを確認しました。)
>>> temporary['result']
0 1 2 3 4 5 6 7 8
0 2025-04-01 00:10:00 0.0 0.7 77 0.3 西北西 0.5 北西
1 2025-04-01 00:20:00 0.0 1.0 77 0.5 北西 1.0 西北西
2 2025-04-01 00:30:00 0.0 1.0 78 0.5 南南西 0.9 西
3 2025-04-01 00:40:00 0.0 0.9 81 0.8 南 1.0 南南西
4 2025-04-01 00:50:00 0.0 0.3 83 0.7 南西 1.1 南南西
.. ... ... ... .. ... ... ... ... ..
しかし、検証の中でよくよく確認していくと構文を書いた状態で実行してもasyncioが動いていない疑惑がでてきました。
インポートでエラーは出ないので利用できるはずが、公式ドキュメントにある例文がVisual Studioでは構文エラー、VSCodeではなぜか実行エラーになることが発覚しました。
import asyncio async def main(): print('hello') await asyncio.sleep(1) print('world') asyncio.run(main())
>>> import asyncio
>>> async def main():
File "<stdin>", line 1
async def main():
^
IndentationError: expected an indented block
>>> print('hello')
hello
>>> await asyncio.sleep(1)
File "<stdin>", line 1
SyntaxError: 'await' outside function
>>> print('world')
world
>>> asyncio.run(main())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'main' is not defined
>>>
おそらくライブラリに原因があるのではなく、インデントを外すと実行できているので、エディタ上でasync defが認識されていないのが原因と思われます。
async def main():print('hello') asyncio.run(main()) async def main():await asyncio.sleep(1) asyncio.run(main())
>>> async def main():print('hello')
>>> asyncio.run(main())
hello
>>>
>>> async def main():await asyncio.sleep(1)
>>> asyncio.run(main())
>>>
いやいやいや…インデントなしでどう記述するんじゃーヽ(`Д´)ノプンプン
fletを使いたい場合Python3.9が必要なので、これ以上バージョンを下げることができず、今回のことからasyncioを使った細かい調整は難しいということが分かりました😓
残念でなりませんが、これをもってアプリケーションの主要部分は完成ということですね(笑)