discord.py でCog・Extensionを使ってみよう!

2023/07/31に公開

はじめに

こんにちは、@nano_sudoです。
discord.pyでBotを作るとき、コードが長くなってしまうことがあります。
そんなときに便利なのが、Cog・Extensionです。
今回はdiscord.pyのCog・Extensionの使い方を具体例を交えて説明していきます。

知っていると良い知識

  • pythonの関数やクラス、デコレーターなど
  • 非同期処理と同期処理の違い

Cogとは

Botを機能ごとに分けることができる機能です。
以下はdiscord.py公式ドキュメントの説明です。

Bot開発においてコマンドやリスナー、いくつかの状態を一つのクラスにまとめてしまいたい場合があるでしょう。コグはそれを実現したものです。(discord.pyドキュメント)

Cog・Extensionを使うメリット

  • 機能のグループ化
    Cogを使用することで、Botの機能をグループに分けることができる。
    (例:音楽コマンドや管理コマンドなど)
  • 再利用性の向上
    Cogをコピーすれば、他のボットに同様の機能を簡単に実装することができる。
  • 柔軟性
    Extensionを利用するとCogの有効化や無効化を簡単に行えるので、必要に応じてBotの動作を変更することができる。
  • 開発の快適性
    Extensionにはホットリロード機能がついているので、開発がより快適にできるようになる。

Cogの最小構成

コマンドは従来のcommands.command()ではなく、app_commands.command()を使います。
app_commandsでは、discordクライアントにコマンドの候補を表示することができます。

Cogを分ける基準

機能ごとに分けるのがおすすめです。
例:音楽コマンド、管理コマンドなど

ファイル構成

.
├── main.py
└── cogs
    └── hello.py
cogs/hello.py
from discord.ext import commands
from discord import app_commands

# commands.Cogを継承する
class MyCog(commands.Cog):
    def __init__(self, bot)
        self.bot = bot
    
    # イベントリスナー(ボットが起動したときやメッセージを受信したとき等)
    @commands.Cog.listener()
    async def on_ready(self):
        print("Cog ready!")
	
    # コマンドデコレーター(descriptionで説明が書ける)
    @app_commands.command(name="hello")
    async def hello(self,interaction:discord.Interaction)
        await interaction.response.send_message("hello!")

コードの解説

Cogの定義

class Hello(commands.Cog): # 好きな名前でOK(機能がわかる名前にすると良い)

commands.Cogを継承することで、Cogを定義することができます。

コンストラクター

def __init__(self,bot) # 自動的にbotが渡される
    self.bot = bot

イベントリスナー

@commands.Cog.listener()
async def on_ready(self):
    print("Cog ready!")

@commands.Cog.listener()デコレーターをつけることで、イベントリスナーを定義することができます。
ほかにも、on_messageon_member_joinなどのイベントリスナーがあります。

コマンドデコレーター

@app_commands.command(name="hello")
async def hello(self,interaction:discord.Interaction):
    await interaction.response.send_message("hello!")

@app_commands.command()デコレーターをつけることで、コマンドを定義することができます。
description引数でコマンドの説明を書くことができます。
name引数と関数名が一致していないと、エラーが発生するので注意です。

(Tips) ハイブリッドコマンド

@app_commands.command()@commands.hybrid_command()に変えると、
スラッシュコマンドと'!'などで呼び出すコマンドの両方を定義できます。
その際、interaction引数をctxに置き換えてください。(一般的に、hybrid_commandでは第一引数はctxと書きます。)

@commands.hybrid_command(name="hello"):
async def hello(self,ctx:commands.Context)
    await ctx.send("hello!")

Cogの有効化

main.py
from discord.ext import commands
from cogs.hello import MyCog
import discord

async def main():
    # Botを定義
    bot = commands.Bot(command_prefix="!",intent=discord.Intents.all())

    # on_readyの前に実行されるイベントリスナー
    @bot.event
    async def setup_hook(self) -> None:
        guild_ids = [123456789012345678] # すぐに同期したいサーバーのIDを入れる
        await self.tree.sync(guild=None)
        for g in guild_ids:
            try:
                self.tree.copy_global_to(guild=g)
            except discord.errors.Forbidden:
                # やりすぎるとForbiddenになるので、一応例外処理を入れておく
                print(f"サーバーID:{g}に登録できませんでした。")

    # Cogを有効化
    await bot.add_cog(MyCog(bot))
    # Botを起動
    await bot.start("token")

if __name__ == "__main__":
    asyncio.run(main())

bot.add_cog()でCogを使ってCogを有効化することができます。
グローバルにコマンドを同期するには、guild=Noneにします。(反映に20分ほどかかります。)
(2024/01/07追記)
Botのクラスを継承せずに書く方法に変更しました。

ExtensionからCogを有効化(おすすめ)

Extension経由で読み込むと、ホットリロードや有効化・無効化が簡単にできるので、おすすめ

cogs/hello.py
# 以下をファイルの最後に追加
async def setup(bot):
    await bot.add_cog(MyCog(bot))

main.pyを以下のように書き換えます。

main.py
from discord.ext import commands
import asyncio
import discord

async def main():
    # Botを定義
    bot = commands.Bot(command_prefix="!",intent=discord.Intents.all())

    # on_readyの前に実行されるイベントリスナー
    @bot.event
    async def setup_hook(self) -> None:
        guild_ids = [123456789012345678] # すぐに同期したいサーバーのIDを入れる
        await self.tree.sync(guild=None)
        for g in guild_ids:
            try:
                self.tree.copy_global_to(guild=g)
            except discord.errors.Forbidden:
                # やりすぎるとForbiddenになるので、一応例外処理を入れておく
                print(f"サーバーID:{g}に登録できませんでした。")

    # Cogを有効化
    await bot.load_extension("cogs.hello")
    # Botを起動
    await bot.start("token")

if __name__ == "__main__":
    asyncio.run(main())

(Tips) フォルダ内のCogをすべて読み込む

大半の方は、cogsフォルダを作って、そこに入れたCogをos.listdir()などでforを回して読み込むと思います。
以下のように書くと、cogsフォルダ内のCogをすべて読み込むことができます。

main.py
import os # ファイルの一番上に追加
# フォルダ内のファイルを読み込む場合(await bot.load_extension()を以下のように書き換える)

for cog in os.listdir("cogs"):
    if cog.endswith(".py"):
        await bot.load_extension(f"cogs.{cog[:-3]}")

# もしくはasyncioのgather(体感速度は同じ)
asyncio.gather(*[bot.load_extension(f"cogs.{cog[:-3]}") for cog in os.listdir("cogs") if cog.endswith(".py")])

まとめ

いかがでしたか?
今回はdiscord.pyのCog・Extensionの使い方を説明しました。
discord.pyを使ってBotを作るときは、ぜひCog・Extensionを使ってみてください!
質問やご意見・ご指摘などあれば、Xかコメント欄にお願いします!

Discussion