🙄

理解して覚えるpython ~デコレータ編~

2022/09/25に公開約4,700字

はじめに

初学者がデコレータだけ学習しようとすると苦戦します.書き方を覚えたつもりでも理解をしていないのでは,毎日書かない限りほんの数日で忘れてしまいます(過去の僕がそうです).
本記事では,デコレータを理解するのに必要な最低限の知識と,そのユースケースを見ていくことで理解して覚えることを目的としています.

前提知識

まず,関数内関数とクロージャから学びます.

関数内関数

簡単に言うと,関数のネストです.ユースケースとしては,クラスを書くほどじゃないけど,少し読みにくい処理等を書きたいとき程度に考えています.関数の外に書けばいいじゃないかと思うかもしれませんが,外側に書くと,他の関数からもアクセスできると誤解されてしまう可能性があり,可読性を下げてしまいます.

def outer(a):
    def inner(b):
        return 3*b
    return inner(a)    
print(outer(1))  # 出力は3

return文でinner()が実行されている点がポイントです.この例では,何も関数内関数である必要はなく,単にイメージとして載せています.より具体的な使いどころとしては,この方の記事が参考になると思います.

クロージャ

クロージャは,内部で定義された変数と引数以外の変数(自由変数と呼ぶ)を覚えている関数のことを指し,ふつうの関数もこれにあたるようです.
関数内関数で示した例をまねると,以下のようになります.

def outer(a):
     def inner(b):
         return 2*a + b
     return inner

ここで注目すべきは,innerが実行されていないまま返されていることです.
このままでは意味がわからないと思いますので,実行してみます.

print(outer(1))
# 出力:<function outer.<locals>.inner at 0x7f568315a4d0>

はじめてこれを見たとき混乱しました.ここでは見た目通り,a に1が代入されていますが,inner関数オブジェクトが返ってきているので,inner関数は実行されていません.ここで重要なのが,inner関数が a の値を覚えているということです.a はinner関数内部で定義されていませんし,引数でもありませんから,上述した「自由変数」に当たります.inner関数はクロージャであるため,外側のスコープで定義された a を覚えているということです.
したがって,inner関数の a に値が保存された状態で実行待ち状態になっているということです.
ここで,次に

tmp = outer(1)
tmp(2)  # 4(=2*1+2) 

# 以下でも同様
# outer(1)が,aに1が代入されたinnerとなっている
outer(1)(2)  # 4(=2*1+2)

を実行すると,inner(2)となり,ようやく関数が実行されたことになります,
ちなみにユースケースは,変数を保存しておいて後で実行したいときですが,そのようなときがパッと浮かばないので他の記事で探してみてください.

デコレータ

いよいよ本題です.

def outer(f):
    def inner(b):
        print('Hello') # プラスの処理(デコレート)部分
        return f(b)  
    return inner

クロージャとの違いは,outer関数の引数が関数fであることです.「outerの引数を覚えておいて,innerを後から実行できる」クロージャの性質を思い出すと,「関数fを覚えたまま保持し,あとからinnerで実行できる」ことがわかります.「outerの引数を覚えておいて,innerを後から実行できる」クロージャの性質を思い出すと,「関数fを覚えたまま保持し,あとからinnerで実行できる」と解釈できます.
ここからがデコレータの本領なのですが,inner関数の内部にプラスの処理を入れておけば関数fに加えて実行する処理を増やすことも可能です.つまり,処理したい関数fをデコレート(修飾)できるということですね.(上の例では関数の実行直前に'Hello'を出力します)
あとはどのように実行するかですが,クロージャと同様に

tmp = outer(f)  # 関数を記憶
tmp(3)  # 実行

としてもいいですが,偉い人たちがデコレートっぽい書き方を提供してくれており,デコレートしたい関数に

@outer
def f(arg)
    return arg

のように,「@関数名」と書くことで,あとは以下のように直接関数fを呼び出すことで実行できるようになっています.

f(3) 
# 出力:
# Hello
# 3

ちゃんと'Hello'が呼び出された後に,関数が実行されている様子がわかります.

少し応用

はじめは読み飛ばしてもいいかもしれません.

functools.wrapsの使用

先ほどの実装では細かい点ですが,問題点があります.f関数の__name__属性を参照してみると,

print(f.__name__)
# 出力:inner

となっています.詳しいことがわからなくても,__name__属性を参照する機能に影響がでそうだとわかります.そこで,関数の名前を適切に参照できるようにしてくれるもの(functools.wraps)を使ってデコレートします.
これを使用して先ほどのデコレータを定義し直します.具体的には,__name__がくるっている関数にデコレートします.

from functools import wraps
.
def outer(f):
    @wraps(f)  # __name__属性の参照先をfにする
    def inner(b):
        print('Hello')
        return f(b)
    return inner

となり,出力は以下のように正常になっています.

print(f.__name__)  
# 出力:f

引数を加える

先ほどのfunctools.wrapsでも気になったかもしれませんが,デコレータには引数を加えることでより柔軟な処理が行えます.
例えば,bool型のis_public変数がTrueのときとFalseのときで記事を公開するか否かを決定したい場合は,以下のようにします.

def outer(is_public=False):
    def middle(f):
        def inner(b):
	    if is_public:
	        # 記事を公開にする処理
	    return f(b)
	return inner
    return middle	

これには,クロージャの性質を利用しています.つまり,普通のデコレータの外側に引数を持った関数でラップすることで,引数として入っているis_public記憶したまま中の処理を行えるということです.

実際の例

組み込み関数

classmethod と staticmethod が有名ですが,これらは以下のように使用します.

class TmpClass:
    CLASS_ = 100  # クラス変数
    
    def __init__(self, instance_var):
        self.instance_ = instance_  # インスタンス変数
    
    @classmethod
    def tmp1(cls): # cls変数でクラス変数にアクセス可能
        print(cls.CLASS_)  # クラス変数なのでアクセス可能
	# print(self.instance_)  # これは不可
	
    @staticmethod
    def tmp2(prm):  # 適当なprmという変数を引数にとる
        print(prm)  # クラス変数,インスタンス変数のどちらでもないのでアクセス可能

classmethodでデコレートされたメソッドは,インスタンス変数のみにアクセスできず,staticmethodは,デコレートされたメソッドの引数やメソッド内で定義された変数,関数以外にアクセスできません,
これらを使用する利点として,名前空間やメモリの削減,何より可読性と保守性の向上が考えられます.

おわりに

割と丁寧に追っていったつもりです.僕自身も忘備録として書いていますので,嘘があった場合には優しくご指摘いただけますとうれしいです.

参考

Discussion

ログインするとコメントできます