Pythonでデコレーターで作った関数からデコレーターを派生させる

yupix

まずはじめに

タイトルで????となった方も多いでしょう。正直私の知識ではうまく言い表せないので変な違和感を覚えること請け合いです。 実際にコードを見てもらいましょう。今回の例はDiscord.pyのCogなどを使用できる CommandFrameWorkを用いた際にできるサブコマンドの作り方です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

class (commands.Cog):
    def __init__(self, bot):
        self.bot = bot
    
    @commands.group()
    async def main():
        pass

    @main.command()
    async def sub():
        pass

さて、いいたいことは伝わったでしょうか?ようは commands.group デコレーターをつけた main関数から main.commandデコレーターを作成する方法になります。

どうやるの?

では完成品からお見せします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Command:
    @classmethod
    def command(cls):
        def wrapper(func):
            return func()

        return wrapper


def group(name=None):
    def wrapper(func):
        func()
        return Command

    return wrapper


@group()
def test():
    print("test")


@test.command()
def sub_test():
    print('test2')

test()

では、完成品をお見せしたところでどのような原理になっているのかの解説です。

!!! warning ここからは完璧な私の独学によるもので、あっているのかいまいちわからない解説です。その点をご理解の上ご覧ください。

まず以下のコードをご覧ください。

1
2
3
4
5
6
def group(name=None):
    def wrapper(func):
        func()
        return Command

    return wrapper

このコードは group というデコレーターを作成している部分になるのですが、この時点で大事なところがあります。それは一番最初に引数で func を受け取っていないことです。 Googleなどで デコレーター 作り方 などと調べてみた際に一般的に出てくるデコレーターは以下のようなコードだと私は思っています

1
2
3
4
def group():
    def decorator(func):
        return func
    return decorator()

ではなぜ今回のコードでは、一番最初に name を受け取っているのでしょうか。そこから解説します。
今回のコードでは、2つのデコレーターを作成する必要があります、1つは group 次に Command クラスにある command デコレーターです。 そして、 group デコレーターでは今回の仕様上 Commandを呼び出す必要があります。そうしたら先程の検索で出てきたコードで再現してみましょう

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Command:
    @classmethod
    def command(cls):
        def wrapper(func):
            return func()

        return wrapper


def group():
    def wrapper(func):
        func()
        return Command

    return wrapper


@group()
def test():
    print("test")


@test.command()
def sub_test():
    print('test2')

test()

>>> group() missing 1 required positional argument 'func' # あっているかイマイチ覚えてません

さて、実装してみました。おや?一番最後にエラーが出ていますね…なぜでしょう。これは デコレーターを 呼び出す際に 関数として成り立つ都合上 func がなくなるからだと考えています。 これを解決するにはどうしましょう、そうです、引数を最初に受け取ればいいですね、ではまた調べてみましょう。 python デコレーター 引数 そしたら出てくるコードはこんな感じでした。

1
2
3
4
5
6
def group(name=None):
    def _group(func):
        def decorator():
            return Command
        return decorator
    return _group

さて、すごいネストしてますね、これの何が問題かというとこれでは先程言った group を呼び出す都合上 確かに _group までは行きますが、 decorator が呼び出されなくなってしまいます。 じゃあ、どうするかというと最初のコードに戻ることです。これを解決するのにとても時間がかかりました。これさえ乗り越えれば簡単です。

なんで Command クラスの commandデコレーターは classmethod?

これはいくつかの理由があります、まず通常のクラスで作ると次に示す2つめのコードになります。 これの何が問題なのでしょうか、これは実行してみるとよく分かることなのですが、@test.command() の部分で selfが渡されていないと怒られてしまいます。 そのため、classmethod にしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Command:
    @classmethod
    def command(cls):
        def wrapper(func):
            return func()

        return wrapper


def group(name=None):
    def wrapper(func):
        func()
        return Command

    return wrapper

@group()
def test():
    print("test")

@test.command()  # 問題なく動く
def sub_test():
    print('test2')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Command:
    def command(self):
        def wrapper(func):
            return func()

        return wrapper


def group(name=None):
    def wrapper(func):
        func()
        return Command

    return wrapper

@group()
def test():
    print("test")

@test.command()  # selfが無いと怒られる
def sub_test():
    print('test2')

ラストに testを実行

さて、これで終わりです。最後に test 関数を追加しましょう!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Command:
    @classmethod
    def command(cls):
        def wrapper(func):
            return func()

        return wrapper


def group(name=None):
    def wrapper(func):
        func()
        return Command

    return wrapper


@group()
def test():
    print("test")


@test.command()
def sub_test():
    print('test2')

test()

動きましたね!

最後に

何かわからない点があれば github の方に issueを建てていただけるとありがたいです! 今回のは記事で見たことがなかったので一応書いてみました、既存のものがあったらごめんなさい! それではまた別の記事でお会いしましょう〜

Built with Hugo
テーマ StackJimmy によって設計されています。