デコレータ先行型にだけ@省略記法があるのはなんで?

こないだのデコレータ奮闘記はおおむね無事閉幕したのだけれど、ひとつ疑問が残っていた。デコレータというロジックが登場する場面には汎用関数先行型とデコレータ先行型があるが(ネーミングは俺)、なんで後者にだけ@の省略記法があるんだろう? ってものだ(先回の一番最後に書いたやつ)。この件に自分なりの解決がつけられたので書く。

みどりんが呼ぶところの汎用関数先行型

def call():
    # デコるプログラム

def deco(func):
    # call()をデコるデコレータ

call = deco(call)

みどりんが呼ぶところのデコレータ先行型

def deco(func):
    # デコレータ

# 省略記法
@deco
def call():
    # デコるプログラム

それはだな…

デコレータが登場する場として二通りあるってのは間違いないと思うんだけど、それぞれ、デコレータを作る人間の立場が違うんだ。

  • 汎用関数先行型ではデコレータを作る人間はデコレータの「製作者」。
  • デコレータ先行型ではデコレータを作る人間はデコレータの「利用者」。

省略記法ってのは、利用者が便利になるように存在するものだ。だから「利用者」を対象とした場面にしか@の省略記法が存在しないんだ。

てかそもそも汎用関数先行型って、デコレータがデコレータである意味が薄いだろう。上のスクリプト例でいえば、デコレータが「call()をデコるデコレータ」ってなってるけど、それはただのcall()の拡張にすぎないだろう? クリスマスツリーのデコレーションってあるけど、あれはクリスマスツリーにしか使えないんだからもはやクリスマスツリーの一部だろう? いや例示に失敗してる感が否めないがそういうことだと思うんだ。特定の関数だけを装飾するためのデコレータはその関数の拡張に過ぎない。デコレータじゃない。だったらデコレータの省略記法も存在しないよそりゃ。

一方、デレコータのセットが用意されてて、みなさんの好きな関数をデコレーションして楽しんでねーって提供されてたら、それは完璧にデコレータだ。プレゼントの包装紙は何にでも使える。何でもラップできる。だから利用者に向けてキレイな包み方とか簡単な包み方もまた提供されてる。それがデコレータの省略記法だ。

 

汎用性のあるデコレータ1 @記法バージョン

ところで、だとすれば、デコレータは汎用性に富んだ書かれ方をすべきだろう。俺が先回から書いてたやつはまさにcall()の拡張にすぎなかったから、めっちゃ抽象度の低い作りだったよな。デコレータ自体に引数を取らせたり、可変長引数に対応したデコレータの書き方もついでに覚えておくぜ。

例として、関数をデコって返り値の文字数を数えるデコレータを作ってみる。ほんで、デコレータ自体にTrueを与えたらスペースも数えて、Falseを与えたらスペースはカウントしない、って作りにする。なんかねえ、デコレータ自体に引数を渡すには、デコレータをさらにネストして定義しないといけないらしいぜ。

def deco_wrapper(count_space):
    def deco_count(func):
        def func_kari(*args):
            if count_space:
                return len(func(*args))
            else:
                return len(func(*args).replace(' ', ''))
        return func_kari
    return deco_count

可変長引数の対応は、単にネストされたfunc_kariの引数部分を全部*argsにするだけだぜ。使い方が以下。

@deco_wrapper(True)
def call1(name):
    return name

@deco_wrapper(False)
def call2(name1, name2):
    return name1 + name2

# count_spaceをTrueにしたので結果は5
print(call1('x x x'))

# count_spaceをFalseにしたので結果は6
print(call2('x x x', 'y y y'))

オッケー、これはそこそこ汎用性がありそうだぜ。ってアレ? こういうデコレータ定義の仕方だと、@省略記法を使わない書き方には対応できねーんじゃねーか?

 

汎用性のあるデコレータ2 @使わないバージョン

疑念は的中し、@記法を使わず、デコレータに引数を渡したい場合は以下のようなデコレータ定義をしねーといけない。

def deco_count(func, count_space):
    def func_kari(*args):
        if count_space:
            return len(func(*args))
        else:
            return len(func(*args).replace(' ', ''))
    return func_kari

使うときはこう。

def call3(name):
    return name

def call4(name1, name2):
    return name1 + name2

# @記法はこのへんを簡略化してる。
call3 = deco_count(call3, True)
call4 = deco_count(call4, False)

# count_spaceをTrueにしたので結果は5
print(call3('x x x'))
# count_spaceをFalseにしたので結果は6
print(call4('x x x', 'y y y'))

あ、こっちはデコレータがさらにネストされることもないし、定義したとおり、見たまんまの動きになるから好みかもしらん。ってアレ? こっちはこっちで@記法に対応できねーんじゃねーか?

@deco_count(True)
def call5(name):
    return name

これはエラーになる。

 

デコレータ奮闘記結論

いやつまり、これはつまり、@使用のデコレータと@不使用のデコレータはてんで別物ってことになるよな!? 何が「省略記法」だ。省略じゃないよコレ。まるで別物だよ。つーわけで今回の奮闘の結論は以下。

  • 俺が「汎用関数先行型」と呼んでたデコレータはデコレータじゃなくてただの関数拡張
  • 俺が「デコレータ先行型」と呼んでたものが正当なデコレータ(何が先行型だ。デコレータは先行するものなのだ)
  • 俺が「@省略記法」って呼んでたのは大間違い。まったく省略じゃない
  • デコレータの書き方には「@記法」と「見たまんま記法」があるが、それぞれデコレータの定義が別物
  • デコレータを作るときはどっちの記法で使わせるか先に決める必要がある

おつかれさまでしたァ!

以下の記事からリンクされています