質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.49%

python3 asyncの動作が分からない

解決済

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 1,513

score 1

前提・実現したいこと

ラズベリーパイ3上でdiscord.pyを動かし、discordでコメントにGPIO出力で反応したり、GPIO入力があればbotがコメントする物が作りたいです。

発生している問題・エラーメッセージ

discord.py 1.4.0a ドキュメント(https://discordpy.readthedocs.io/ja/latest/quickstart.html)のサンプルコードを元に改変してbotを動かしてコメントに反応したり、コメントに応じてGPIOから出力することは出来ましたが、async関数の動きがよく分からずGPIO入力に応じてbotにコメントさせる方法が分かりませんでした。

該当のソースコード

import discord
import time
import RPi.GPIO as GPIO
import sys

GPIO.setmode(GPIO.BOARD)
GPIO.setup(3, GPIO.IN, pull_up_down=GPIO.PUD_UP)#ボタン1
GPIO.setup(7, GPIO.OUT)#サーボ
GPIO.setup(11, GPIO.IN, pull_up_down=GPIO.PUD_UP)#ボタン2
GPIO.setup(12,GPIO.OUT)#インジケータLED

p = GPIO.PWM(7, 50)
p.start(0.0)

client = discord.Client()

@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))

@client.event
async def on_message(message):
    if message.author == client.user:
        return

    if message.content.startswith('サーボ上げる'):
        await message.channel.send('下げました!')
        p.ChangeDutyCycle(10)
        time.sleep(1.0)
        p.ChangeDutyCycle(0.0)
        return

    if message.content.startswith('サーボ下げる'):
        await message.channel.send('下げました!')
        p.ChangeDutyCycle(5.1)
        time.sleep(1.0)
        p.ChangeDutyCycle(0.0)


client.run('BotのID')

試したこと

async def on_message(message)はメッセージが来たときのみ作動するようだったので、見よう見まねで新しくasyncを作ってみましたが動作しませんでした。
新しくループを作り、その中にon_messageとon_readyとGPIO入力を入れてやれば動作するのかなとは思っていますが、動作がよく分からず難航しています。

補足情報(FW/ツールのバージョンなど)

Raspberry Pi3 modelB(Raspbian Buster)
python 3.7.3

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

※ GPIO は使ったことがないので、一般的な「非同期」と「同期」の間の連携方法として回答します。

まず前提として discord.py は非同期IOで扱われている為、
asyncioのイベントループ外から直接メッセージを送る事は出来ません。
解決案として提示できるのは以下の2通り。

  • A. 「別スレッド」から「非同期ループで処理される関数」にメッセージを送る方法
    非同期<=>同期 をサポートした Queue の実装。 "janus" ライブラリを使う
  • B. GPIO の入出力を非同期IOで扱うライブラリ "apigpio"

末尾のコードで主にAの方法について解説します


A. 「別スレッド」から「非同期ループで処理される関数」にメッセージを送る方法

概要:

- 非同期<=>同期間連携の為のQueue を準備
  - janus のインストール (pip install janus)
- 同期処理
  - GPIO: callback で通知を受け取る (別スレッドで実行される)
    - Queue へメッセージ送信(put)
- 非同期処理
  - Queue からの値を読み出す(get)処理のループ
  - 通常の discord の処理のループ
  - asyncio.gather で複数の非同期ループを纏める
  - asyncio.run 非同期処理のイベントループを開始

B. GPIO を非同期IOで扱うライブラリ "apigpio"

"pigpio" というライブラリがありそれを非同期に扱う為のモノの様です。

apigpio のサンプルコードを読む上での注意点:

サンプルコードの対応しているPythonのバージョンが古いスタイルの記述になっています。
実行には支障ないと思いますが以下のように読み替えます。

  • @asyncio.coroutine -> async
  • yield from -> await

実際に試してないので軽く紹介のみ。

yield が何かわからない場合、
前提知識として「ジェネレータ」辺り迄は理解しておいた方が良いと思います。

全体像はこのような感じになります(概要のみ)

  • apigpio GPIOを扱う非同期IO
  • discord を扱う非同期IO (client.runの代わりにclient.startを利用)
  • NOTE: 非同期同士の連携には asyncio.Queue が使えます。
  • asyncio.gatherで2つの非同期処理をまとめる (後述のコードを参考にして下さい)
  • asyncio.runで開始

async/await について

大雑把な説明です。実際のコーディングの際の注意点に焦点を当てました。

  • async は、awaitを用いる関数 の定義等で使います。async付きで定義された関数を呼び出すと「コルーチン」を返します。

asyncを付けると関数自体が自動的に非同期になったりは しません (よくある誤解1)
関数内のコードの実行自体は通常と同じように処理されます。
その為、非同期処理の中ではコードの実行を止めるような操作はNG
(エラーにはなりませんが、他の非同期処理が出来なくなり期待する動作になりません)

非同期(async)の関数(コルーチン)内で出来ない事:

  • time.sleep等のコードの実行を止める操作。同一スレッド内ではNG
  • await を伴わない無限ループ。await がないとその間他の非同期処理が実行されません。
  • ブロッキング I/O 操作 (ファイル書込やsocketからの読書等)

  • await では、値が返されるまで処理を待ちます。

但し、await を付けると自動的にその部分で待つようになったりは しません (よくある誤解2)
awaitの右側は、await可能な(awaitable)オブジェクトである必要があります。
具体的には「コルーチン」「フューチャー」「タスク」等。説明は省きますが
重要な点は、 await で使えるという共通の性質(awaitable)を持ちます。


  • asyncio.run (client.run内部)

非同期処理のイベントループの正体ですが、単一スレッド上で動作するループです。
各非同期処理(コルーチン)自体は、通常の関数と同様に実行されます。

順番に実行される

  • 非同期処理A await で イベントループに戻る
  • 非同期処理B await で イベントループに戻る
  • 非同期処理A 以前の await から再開、次のawait でまた イベントループに戻る

繰り返し説明になりますが、今迄「非同期処理」でやってはいけない事として説明した理由がここで、
各非同期処理を実行する為に、イベントループは動き続ける必要があります。
つまり、非同期処理内では、コードの実行を中断するようなブロッキング操作をしてはいけない。

これは、GUIプログラミングに於いてのイベントループと同じで、
イベント処理内で時間のかかることをすると、GUIがフリーズするといったように、
非同期処理内でも同様、時間のかかる処理をすると、その間他の非同期処理が実行されないという事が起こります。


"""
このプログラムは、非同期で処理されるdiscord と
別スレッドで動作するGPIOのコールバックとの間でのメッセージ受け渡しを再現するデモです。

非同期IOとスレッドの連携のデモとして書いたコードなので、
実際のdiscord/GPIOの利用には手直しが必要になる点をご了承ください。

discord, GPI は用いず、動作を模倣した仮のクラスを作って行います。
このコードの実行には discord.py と RPi.GPIO のインストールは不要です。


事前にインストールが必要な外部ライブラリ: "janus"

非同期/同期での読書が可能なQueueの実装です。

- janus(https://pypi.org/project/janus/) ライブラリ
  pip install janus でインストール
"""

import time
import asyncio
from threading import Thread

from janus import Queue

# ---------- 最初の MockGPIO, MockDiscord は意図的に読み飛ばしてください --------

class MockGPIO(Thread):
    """
    NOTE: GPIO.start() 時に run() が呼び出される。
    """

    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self._callback = None

    def run(self):
        # 別スレッドで稼働。ここではブロッキング操作(time.sleep)は可能
        for num in range(10): # <-- 仮の入力データ [0..9]
            if self._callback:
                self._callback(num)
            time.sleep(1)

    def add_event_detect(self, callback=None):
        if callback:
            self.add_event_callback(callback)
        # ここは実際にも、add_event_detectを呼び出す事で、新スレッドが立ち上がります。
        self.start()  # Thread.start -> 別スレッドでrun()を実行

    def add_event_callback(self, callback):
        self._callback = callback


class MockDiscordClient:

    def __init__(self):
        self._callback = None

    async def run_event(self, *args, **kw):
        await self._callback(*args, **kw)

    async def start(self, *args):
        # 非同期で動作。
        # 通常のtime.sleep 等、プログラムの実行を阻害する(ブロッキング)操作は、
        # asyncioのループを止めてしまうのでNG。非同期での代替の手段を取ります。
        for num in range(10): # <-- 仮の入力データ [0..9]
            await asyncio.sleep(1)
            await self.run_event(num)

    def event(self, coro):
        if asyncio.iscoroutinefunction(coro):
            self._callback = coro
        return coro

    async def send(self, msg):
        print("send to discord >>>", msg)
        await asyncio.sleep(1)


GPIO = MockGPIO(daemon=True)
client = MockDiscordClient()

## 上は GPIO モジュールと discord をテスト用にエミュレートする為のクラスです
## ユーザ視点からは中の実装をそれ程詳細に把握する必要はありません。

## 実際の実装に合わせて模倣している要点は2つ
## * discoard は非同期IO(asyncio)で扱われる
## * GPIO のコールバックは別スレッドで動いている

# ------------------------ 解説ここから ------------------------

async def main():

    ##
    # (1) キューを用いてメッセージのやり取りの(準備)
    #
    # janus.Queue (sync <=> async queue)
    queue = Queue()
    sync_q = queue.sync_q # GPIO側で使う同期キュー
    async_q = queue.async_q # awaitで使う非同期キュー

    ##
    # (2) GPIO からの入力に応じてコールバックを呼び出し
    #
    # ※ GPIO については簡略化してるので、実際の呼び出し方と異なります。
    # 実際の呼び出し方は add_event_detect, add_event_callback 辺りを調べて見て下さい。
    #
    # ※ 更に、実際の利用は、discord のon_ready等で discord の準備が出来てから登録する。
    def callback(num):
        # GPIO -> callback -> 同期キューにput
        print("GPIO >>>", num)
        sync_q.put(num)

    GPIO.add_event_detect(callback=callback)

    ###
    # (3) ディスコードのメッセージ受信
    #
    # ※ 実際のdiscordライブラリにこのような名前のイベントはありません。
    # discordのclient と 他の非同期ループを扱う為のデモとして仮実装。
    @client.event
    async def on_discord_message(msg):
        await asyncio.sleep(1)
        print("recv from discord <<<", msg)

    ## 
    # (4) キュー内のメッセージを非同期で読み出し
    #
    # GPIOのコールバックで sync.put で送られたものを async_q.get で読み出します
    #
    async def listen_queue_loop():
        while True:
            msg = await async_q.get()
            # ディスコードへ送信 (※ 実際はチャンネルに送信)
            await client.send(str(msg))

    ##
    # (5) 非同期ループの実行 (client.runの置き換え)
    # - asyncio.gather 2つの非同期ループの実行に用います
    #   - client.start
    #   - listen_queue_loop
    #
    # 非同期処理: ディスコードclient と、キューの読み出しの開始

    BOT_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    await asyncio.gather(client.start(BOT_TOKEN), listen_queue_loop())


if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/05/12 20:47

    とても丁寧な回答ありがとうございます。
    もうここまで解説して頂ければもうゴールは見えているので週末試してみようと思います。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.49%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る