Help us understand the problem. What is going on with this article?

Discord.py wait_for() について

DiscordAPIのPythonラッパーであるDiscord.pyで行われる操作は、基本的に discord.Client インスタンスを元に行われます。
discord.Client には、 wait_for() というメソッドが存在するのですが、これを使うことで特定のイベントを待機することができます。


多分、リファレンス読めば解決することが多いと思います:
https://discordpy.readthedocs.io/ja/latest/api.html#discord.Client.wait_for

が、読んでも解決しない人を頻繁に見かけるので記事にしました。余計に分かりにくくなっている可能性は否めないけど。


もっぱら関数を扱う記事なので、当然引数や戻り値といった関数に付属する概念については既に履修済みであることを想定しています・・・


補足

この記事で使う用語について私のイメージを紹介しておきます。
(この定義はあくまでもDiscord.pyに関連する文脈に限定されます)

イベントとは

DiscordAPI側から通知された、Discord上で発生した事象のこと。
メッセージが送信された場合には message イベント、メッセージにリアクションが付けられた場合には reaction_add イベントが発生する。


イベントにはそれに関連したデータが添えられる
message イベントなら、送信されたメッセージを示す discord.Message インスタンスが添えられている。
reaction_add イベントなら、追加されたリアクションを示す discord.Reaction インスタンスと、リアクションを追加したユーザーを示す discord.User インスタンスが添えられている。


先に、添えられる、と書いたが関連データが一切添えられないイベントも存在する。代表的な例として、BOTの内部的な準備の完了を通知する ready イベントがある。

イベントリファレンス(https://discordpy.readthedocs.io/ja/latest/api.html#discord-api-events )からイベント名とその関連データの一覧を参照できる (重要)。


イベントハンドラとは

イベントに対する処理を記述したもの。
内部的には discord.Client インスタンス自身の属性から on_イベント名 のメソッドを検索し呼び出している。
イベントハンドラとなる関数にはイベントに関連したデータが引数として渡される


@client.event
async def on_message(message):
    pass

この場合、メッセージが送信されたタイミングでイベントハンドラである on_message メソッドが呼び出されるが、このとき関連データである discord.Message引数として渡される


@client.event
async def on_reaction_add(reaction, user):
    pass

この場合、メッセージにリアクションが付与されたタイミングでイベントハンドラである on_reaction_add メソッドが呼び出されるが、このとき関連データである discord.Reactiondiscord.User引数として渡される


wait_for() メソッド

wait_for() メソッドは、特定のイベントが発生するまで待機するコルーチン関数です。
うまく工夫すればイベントハンドラにそのまま記述しても処理できるかもしれませんが、一連の処理は同じ場所に記述したほうが分かりやすいというものです・・・


書式は以下の通りです。

client.wait_for("イベント名",check=チェック用関数,timeout=制限時間)
  • 第一引数の イベント名 は、文字通り待機したいイベント名が入ります(reaction_addなど)。

  • check には、関連データ(reaction_add イベントなら discord.Reactiondiscord.User)を引数として受け取る関数を渡します。
    • 「関連データを受け取る関数」という点で、イベントハンドラと共通していると思います。
  • check 引数に関数が渡されている場合、その関数に該当の関連データを渡して実行します。
    • 関数から True が返却された場合は待機を終了し、次の処理へと移ります。
    • False が返却された場合は待機を続行します。

  • timeout には待機を行う最大秒数を渡します。その秒数を超えて待機を行うことはありません。
    • 指定した秒数を超過した場合には、asyncio.TimeoutError という例外が発生します。
    • 例外なので、try~except 節で例外処理を記述するのが望ましいです(後述)。

なお、checktimeout は省略することができます(イベント名と異なり順不同)。


  • 待機が正常に終了した場合は戻り値としてイベントの関連データが返却されます
    • message イベントならば関連データは discord.Message が返されます。
    • reaction_add イベントなら discord.Reactiondiscord.User が返されます。
    • reaction_add イベントのように関連データが複数存在する場合は、タプルとしてまとめて返されます
    • ready イベントのように関連データが存在しない場合は戻り値もありません。
  • check 引数を指定した場合は、チェックを最初に通過したイベントの関連データが戻り値となります。

コード例

正直APIリファレンスの例の丸パクリなんですよね

ユーザーからの返信の待機

以下のような動作をするコードです。

  1. ユーザーがテキストチャンネルに $greet と送信する。
  2. BOTはこんにちはと送信してね! とそのチャンネルに送信する。
  3. BOTはそのチャンネルで こんにちは と返答がなされるのを待機する。
  4. 待機が完了したら、返答をしてくれた人に ○○(返信者の名前)、こんにちは! と送信する。
@client.event
async def on_message(message):
    if message.content.startswith('$greet'):
        channel = message.channel
        await channel.send('こんにちはと送信してね!')

        def hello_check(m):
            return m.content == 'こんにちは' and m.channel == channel

        msg = await client.wait_for('message', check=hello_check)
        await channel.send(f'{msg.author.mention}、こんにちは!')

以下、wait_for() に関する重要な部分についてのみ抜粋して説明します。


def hello_check(m):
    return m.content == 'こんにちは' and m.channel == channel

ここで、チェック用の関数を定義します。
message イベントに添えられる関連データは送信されたメッセージを示すdiscord.Message インスタンスでしたね。
なので、引数である m に入るのも discord.Message インスタンスになります。
discord.Message インスタンスからは content属性で内容を、 channel属性で送信元のチャンネルを取得できます。

m.content == 'こんにちは' and m.channel = channel

の評価結果は、送信されたメッセージ内容が「こんにちは」かつ、送信元チャンネルが $greet が送信されたチャンネルと同じなら True になりますし、そうでなければ False になります。


msg = await client.wait_for('message', check=hello_check)

本日の主役。
wait_for() の第一引数であるイベント名には message イベントを渡しました。
つまり、メッセージが送信されたタイミングで待機を終了することになります。
しかし、check 引数に hello_check 関数も渡しています。
したがって、メッセージが新たに送信されるたびに hello_check 関数に送信されたメッセージを渡して、戻り値がTrueなら待機を終了し、 msg には送信されたメッセージそのものが代入されます


(実行例)

image.png

リアクションの待機

以下のような動作をするコードです。

  1. ユーザーがテキストチャンネルに $thumb と送信する。
  2. BOTは このメッセージに 👍 リアクションをつけてね! と送信する(1)
  3. (1)のメッセージに、$thumbを送信したユーザーが👍 リアクションを付与するのを待機する。(時間制限は60秒に設定)
  4. もし、リアクションが60秒以内に付与されたら、BOTはそのチャンネルに👍 と送信する。
  5. 間に合わなければ、👎と送信する。
@client.event
async def on_message(message):
    if message.content.startswith('$thumb'):
        channel = message.channel
        sent_msg = await channel.send('このメッセージに 👍 リアクションをつけてね!')

        def reaction_check(reaction, user):
            are_same_messages = reaction.message.channel == sent_msg.channel and reaction.message.id == sent_msg.id
            return user == message.author and str(reaction.emoji) == '👍'  and are_same_messages

        try:
            reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=reaction_check)
        except asyncio.TimeoutError:
            await channel.send('👎')
        else:
            await channel.send('👍')

開発環境によっては reactionuser が未使用の変数として警告されるかもしれませんが、あくまで説明用なので無視していただいて結構です。


def check(reaction, user):
    are_same_messages = reaction.message.channel == sent_msg.channel and reaction.message.id == sent_msg.id
    return user == message.author and str(reaction.emoji) == '👍'  and are_same_messages

チェック用の関数定義です。引数として、reaction には付与されたリアクションを示す discord.Reaction インスタンスが、 user にはリアクションを付与したユーザーを示す discord.User インスタンスが渡されます。

are_same_messages は、リアクションが付与されたメッセージとBOTが送信したメッセージの同一性を示す変数です(同一とみなせるならTrue)。
一行に詰め込み過ぎるのもあれなので、別途変数に分けました。

reaction.emoji は、名前通りリアクションの絵文字を示しています。str() 関数に渡すことで文字列に変換しています(非カスタム絵文字はもともと文字列なので、この変換は必須ではないかもしれない)。

user == message.author and str(reaction.emoji) == '👍'  and are_same_messages

の評価結果は、リアクションを付与したユーザーが $thumbの送信者と同一、かつ、付与されたリアクションの絵文字が 👍 かつ、リアクションが付与されたメッセージがBOTの送信したメッセージと同一だった場合に True になります。


try:
    reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=reaction_check)
except asyncio.TimeoutError:
    await channel.send('👎')
else:
    await channel.send('👍')

返信の待機を行う先の例とは異なり、timeout 引数を設定しています。よって、時間切れになればその時点で待機は強制終了、 例外asyncio.TimeoutError が発生します。
ここでは条件に適したリアクション(関連データを reaction_check に渡した結果がTrueとなるもの)の付与を60秒間待機し、もし制限時間に間に合わなかった(=例外asyncio.TimeoutErrorが発生した)なら👎 を、そうでなければ 👍 を送信します。


補足として、

reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=reaction_check)

についても少し説明します。
wait_for() が正常に待機を終了した場合には関連データを戻り値として返却するのですが、関連データが複数存在する場合は タプルを返却するんでしたね。
ここではアンパックを利用してタプルの要素をreactionuserにそれぞれ分けて代入しています。


(実行例)
image.png
上が時間内にリアクションを付与した場合。下が時間内に付与しなかった場合。

最後に

wait_for() は活用できれば便利な機能だと思います。
開発の支援になればと頑張って書いたんですが説明下手だったらすみません。
何か誤記があれば修正します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした