BETA

【discordpy】issue #516 解読メモ

投稿日:2019-05-03
最終更新:2019-05-05

Python の asyncio(非同期処理、イベントループ)の理解と、
複数の Discord Bot アカウントを稼働させるためのヒントとなるので共有。

出典元概要

以下のような準抽象コードで複数のBotを稼働させようとしたがうまくいかなかった。
何が間違っているのだろうか?
という内容のissue

# Creating the bots  
botA = commands.Bot(command_prefix='!', description = 'Example')  
botB = commands.Bot(command_prefix='?', description = 'Example')  

# Creating the loop  
loop = asyncio.get_event_loop()  

# Then I defined the bots structure, using events, and run it through asyncio library.  
@botA.event  
async def on_ready():  
    print('[BOT] Created.')  

loop.run_until_complete(botA.run('TOKEN_A'))  

@botB.event  
async def on_ready():  
    print('[BOT] Created.')  

loop.run_until_complete(botB.run('TOKEN_B'))  

# And at the end of the script  

try:  
    loop.run_forever()  
finally:  
    loop.close()  

issuecomment-287511041 和訳

今後のために書きます。

初心者は Client.run
2つの下位概念の抽象概念であることを理解する必要があります。

クライアントにログインする Client.login と、
実際に処理を実行する Client.connect があります。
これらはコルーチンです。

asyncio はイベントループに処理を配置し、
時間があれば常に実行するような機能を提供します。

この例のように。

loop = asyncio.get_event_loop()  

async def foo():  
  await asyncio.sleep(10)  
  loop.close()  

loop.create_task(foo())  
loop.run_forever()  

asyncioは別のコルーチンで何かが起こるのを待つために、
asyncio.Event による同期機能を提供します。
これは真偽値を待つものと考えることができます。


e = asyncio.Event()  
loop = asyncio.get_event_loop()  

async def foo():  
  await e.wait()  
  print('完了を待機しています...')  
  loop.stop()  

async def bar():  
  await asyncio.sleep(20)  
  e.set()  

loop.create_task(bar())  
loop.create_task(foo())  
loop.run_forever() # 'e'がtrueに設定された時、fooはこのイベントループを停止させます。  
loop.close()  

また、複数の futures やコルーチンの完了を待つ方法もあります。

a = asyncio.Event()  
b = asyncio.Event()  

async def foo():  
  await asyncio.sleep(10)  
  a.set()  

async def bar():  
  await asyncio.sleep(15)  
  b.set()  

loop = asyncio.get_event_loop()  
loop.create_task(bar())  
loop.create_task(foo())  
loop.run_until_complete(asyncio.wait([a.wait(), b.wait()]))  
loop.close()  

これらの概念を discord bot にも適用できます。

import asyncio  
import discord  
from collections import namedtuple  

# まず、client自身がいつイベントループを完全に閉じるべきかを知るために、  
# botが完全にcloseする時のイベントシグナリングを登録する必要があります。  

Entry = namedtuple('Entry', 'client event')  
entries = [  
  Entry(client=discord.Client(), event=asyncio.Event()),  
  Entry(client=discord.Client(), event=asyncio.Event())  
]  

# それから、全てのclientにloginして、connectコールをラップすることで、  
# 実際にいつ完全にcloseするかを判断できるようにします。  

loop = asyncio.get_event_loop()  

async def login():  
  for e in entries:  
    await e.client.login()  

async def wrapped_connect(entry):  
  try:  
    await entry.client.connect()  
  except Exception as e:  
    await entry.client.close()  
    print('We got an exception: ', e.__class__.__name__, e)  
    entry.event.set()  

# 実際にイベントループを閉じるべきかどうかを確認する必要があります。  
async def check_close():  
  futures = [e.event.wait() for e in entries]  
  await asyncio.wait(futures)  

# いつloginするかの記述です。  
loop.run_until_complete(login())  

# ここで全てのclientに接続します。  
for entry in entries:  
  loop.create_task(wrapped_connect(entry))  

# ここで全てのclientがcloseするのを待っています。  
loop.run_until_complete(check_close())  

# 最後にイベントループを閉じます。  
loop.close()  

お役に立てれば幸いです。

実際に複数のDiscord Bot アカウントを稼働させるコード

issue で提示されているのは準抽象コードなので補完する必要があった。
主にトークン周り。

import asyncio  
import discord  
from collections import namedtuple  
import os  

TOKEN1 = os.environ['DISCORD_BOT_TOKEN1']  
TOKEN2 = os.environ['DISCORD_BOT_TOKEN2']  

class DiscordClient(discord.Client):  
    async def on_message(self, message):  
        if message.author == self.user:  
            return  
        if message.content.startswith('ping'):  
            await message.channel.send('pong')  

Entry = namedtuple('Entry', 'client event token')  
entries = [  
    Entry(client=DiscordClient(), event=asyncio.Event(), token=TOKEN1),  
    Entry(client=DiscordClient(), event=asyncio.Event(), token=TOKEN2)  
]  

async def login():  
    for e in entries:  
        await e.client.login(e.token)  

async def wrapped_connect(entry):  
    try:  
        await entry.client.connect()  
    except Exception as e:  
        await entry.client.close()  
        print('We got an exception: ', e.__class__.__name__, e)  
        entry.event.set()  

async def check_close():  
    futures = [e.event.wait() for e in entries]  
    await asyncio.wait(futures)  

loop = asyncio.get_event_loop()  
loop.run_until_complete(login())  
for entry in entries:  
    loop.create_task(wrapped_connect(entry))  
loop.run_until_complete(check_close())  
loop.close()  

関連ドキュメント

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

備忘録のようなもの

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!