理解して覚えるpython ~デコレータ編~
はじめに
初学者がデコレータだけ学習しようとすると苦戦すると思います.書き方を覚えたつもりでも理解していないと,ほんの数日で忘れてしまいます(過去の僕がそうでした).
本記事では,デコレータを理解するのに必要な最低限の知識と,そのユースケースを見ていくことで理解して覚えることを目的としています.
前提知識
まず,関数内関数とクロージャから学びます.
関数内関数
簡単に言うと,関数のネストです.ユースケースとしては,クラスを書くほどじゃないけど,少し読みにくい処理等を書きたいときとかではないでしょうか(あまり書くことはないと思いますが).内部の関数を外に書かかないことで,他の関数からもアクセスできないという制限をかけており,単純に2つ関数を書くよりも可読性が向上します.
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 関数の引数でもありませんから,上述した「自由変数」に当たります.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 で実行できる」ことがわかります.
ここからがデコレータの本領なのですが,inner 関数の内部にプラスの処理を入れておけば関数 f に加えて実行する処理を増やすことが可能です.つまり,処理したい関数 f をデコレート(修飾)できるということですね.(上の例では関数の実行直前に'Hello'
を出力します)
あとはどのように実行するかですが,クロージャと同様に
tmp = outer(f) # 関数を記憶
tmp(3) # 実行
としてもいいですが,偉い人たちがデコレートっぽい書き方を提供してくれており(syntax sugur
という),デコレートしたい関数に
@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