tl;博士 我的机器人如何异步等待对多条消息的反应?
我要在Discord机器人中添加剪刀布(rps)命令。用户可以调用命令,方法是输入.rps
和一个可选参数,指定要玩的用户。
.rps @TrebledJ
被调用时,机器人将直接调用(DM)调用它的用户和目标用户(通过参数)。然后,两个用户使用✊,或✌️对他们的DM 进行反应。
现在,我正在尝试使其异步工作。具体来说,该漫游器会将DM发送给两个用户(异步),并等待他们的响应(异步)。分步场景:
Scenario (Asynchronous):
1. User A sends ".rps @User_B"
2. Bot DMs User A and B.
3. User A and B react to their DMs.
4. Bot processes reactions and outputs winner.
(另请参见:注释[1])
由于目标是监听来自多个消息的响应,因此我尝试创建两个单独的线程/池。这是三个尝试:
multiprocessing.pool.ThreadPool
multiprocessing.Pool
concurrent.futures.ProcessPoolExecutor
不幸的是,这三个都没有解决。 (也许我实施不正确吗?)
以下代码显示命令功能(rps
),辅助功能(rps_dm_helper
)和3次(失败)尝试。所有尝试都使用不同的辅助函数,但是基本逻辑是相同的。为了方便起见,第一次尝试没有注释。
import asyncio
import discord
from discord.ext import commands
import random
import os
from multiprocessing.pool import ThreadPool # Attempt 1
# from multiprocessing import Pool # Attempt 2
# from concurrent.futures import ProcessPoolExecutor # Attempt 3
bot = commands.Bot(command_prefix='.')
emojis = ['✊', '', '✌']
# Attempt 1 & 2
async def rps_dm_helper(player: discord.User, opponent: discord.User):
if player.bot:
return random.choice(emojis)
message = await player.send(f"Playing Rock-Paper-Scissors with {opponent}. React with your choice.")
for e in emojis:
await message.add_reaction(e)
try:
reaction, _ = await bot.wait_for('reaction_add',
check=lambda r, u: r.emoji in emojis and r.message.id == message.id and u == player,
timeout=60)
except asyncio.TimeoutError:
return None
return reaction.emoji
# # Attempt 3
# def rps_dm_helper(tpl: (discord.User, discord.User)):
# player, opponent = tpl
#
# if player.bot:
# return random.choice(emojis)
#
# async def rps_dm_helper_impl():
# message = await player.send(f"Playing Rock-Paper-Scissors with {opponent}. React with your choice.")
#
# for e in emojis:
# await message.add_reaction(e)
#
# try:
# reaction, _ = await bot.wait_for('reaction_add',
# check=lambda r, u: r.emoji in emojis and r.message.id == message.id and u == player,
# timeout=60)
# except asyncio.TimeoutError:
# return None
#
# return reaction.emoji
#
# return asyncio.run(rps_dm_helper_impl())
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
if opponent is None:
opponent = bot.user
# Attempt 1: multiprocessing.pool.ThreadPool
pool = ThreadPool(processes=2)
author_result = pool.apply_async(asyncio.run, args=(rps_dm_helper(ctx.author, opponent),))
opponent_result = pool.apply_async(asyncio.run, args=(rps_dm_helper(opponent, ctx.author),))
author_emoji = author_result.get()
opponent_emoji = opponent_result.get()
# # Attempt 2: multiprocessing.Pool
# pool = Pool(processes=2)
# author_result = pool.apply_async(rps_dm_helper, args=(ctx.author, opponent))
# opponent_result = pool.apply_async(rps_dm_helper, args=(opponent, ctx.author))
# author_emoji = author_result.get()
# opponent_emoji = opponent_result.get()
# # Attempt 3: concurrent.futures.ProcessPoolExecutor
# with ProcessPoolExecutor() as exc:
# author_emoji, opponent_emoji = list(exc.map(rps_dm_helper, [(ctx.author, opponent), (opponent, ctx.author)]))
### -- END ATTEMPTS
if author_emoji is None:
await ctx.send(f"```diff\n- RPS: {ctx.author} timed out\n```")
return
if opponent_emoji is None:
await ctx.send(f"```diff\n- RPS: {opponent} timed out\n```")
return
author_idx = emojis.index(author_emoji)
opponent_idx = emojis.index(opponent_emoji)
if author_idx == opponent_idx:
winner = None
elif author_idx == (opponent_idx + 1) % 3:
winner = ctx.author
else:
winner = opponent
# send to main channel
await ctx.send([f'{winner} won!', 'Tie'][winner is None])
bot.run(os.environ.get("BOT_TOKEN"))
注释
[1]:将异步方案与非异步方案进行对比:
Scenario (Non-Asynchronous):
1. User A sends ".rps @User_B"
2. Bot DMs User A.
3. User A reacts to his/her DM.
4. Bot DMs User B.
5. User B reacts to his/her DM.
6. Bot processes reactions and outputs winner.
这并不难实现:
...
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
...
author_emoji = await rps_dm_helper(ctx.author, opponent)
if author_emoji is None:
await ctx.send(f"```diff\n- RPS: {ctx.author} timed out\n```")
return
opponent_emoji = await rps_dm_helper(opponent, ctx.author)
if opponent_emoji is None:
await ctx.send(f"```diff\n- RPS: {opponent} timed out\n```")
return
...
但是恕我直言,非异步会导致不良的UX。 :-)
[2]:其他上下文:该机器人托管在Heroku上,并与Python 3.7一起运行。
答案 0 :(得分:1)
您应该能够使用asyncio.gather
安排多个协程同时执行。等待gather
等待它们全部完成并以列表形式返回结果。
from asyncio import gather
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
if opponent is None:
opponent = bot.user
author_helper = rps_dm_helper(ctx.author, opponent) # Note no "await"
opponent_helper = rps_dm_helper(opponent, ctx.author)
author_emoji, opponent_emoji = await gather(author_helper, opponent_helper)
...