先日、面接の際にわかりますか? って聞かれてなんだっけ? となって結局わかりませんと答えてしまったので、情けなく感じたので復習します。
現役の方のツッコミや実践的な例等ご教授ありましたら是非コメント欄へお願いします。
すごく助かります。
結論
デコレータは関数を引数にとって、さらに新たな関数を返すcallable()のようなもの(呼び出し可能であり、関数を引数に取り、それに代わる新たな関数を返すもの)である。
ここから転じて引数の関数の中身を変えずに、実行結果を修飾(語弊があるが感覚としては編集)することができる。
似たようなのにOverrideがあるけれど、こちらは親クラスのメソッドを上書きして子クラス等で使うもので、デコレータを使うのとではその違いが見れる。
以下参考記事の例を参照したものです。
# まず基本のクラス
# 初期値としてx,yの座標を受け取って座標を返すというメソッドを持つ
class Coordinate:
# 初期値セット
def __init__(self, x, y):
self.x = x
self.y = y
# 返り値
def __repr__(self):
return "Coord: " + str(self.__dict__)
# Coordinateクラスのメソッドを新しく返す関数(デコレータ)
def add(a, b):
return Coordinate(a.x + b.x, a.y + b.y)
def sub(a, b):
return Coordinate(a.x - b.x, a.y - b.y)
# ここから実際に使ってみる
# 引数にCoordinateクラスのメソッドで得られた座標を引数として、新たな座標を計算して返してほしい
one = Coordinate(100,200)
two = Coordinate(300,200)
three = Coordinate(-100, -100)
# 結果
add(one, two) # Coord: {'x': -200, 'y': 0}
add(one, three) # Coord: {'x': 0, 'y': 100}
# このように本来xとyの座標を受け取って返すだけだったCoordinate()がある2点の座標を受け取って任意の計算を行い、新たな座標を返す関数として修飾されたことがわかる
# 以下は更に修飾を行う例となる
# 計算結果がマイナス座標だった場合、0へ切り上げる処理を追加している(= 扱う座標系は0が下限である必要があるという条件を満たすようにする)
def wrapper(func):
def checker(a,b):
if a.x < 0 or a.y < 0:
a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
if b.x < 0 or b.y < 0:
b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
ret = func(a,b)
if ret.x < 0 or ret.y < 0:
ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
return ret
return checker
# デコレータであったそれぞれの関数がさらに修飾された形になる証拠が以下の書き方
# これで先程定義したadd()とsub()ではなく、それぞれwrapperの処理が追加された状態で以後は呼び出されることになる
# 結果
add = wrapper(add)
sub = wrapper(sub)
sub(one,two) # Coord: {'x': 0, 'y': 0}
add(one,three) # Coord: {'x': 100, 'y': 200}
で、基本的にはadd = wrapper(add)
な書き方はせず
@wrapper
def add(a,b):
return Coordinate(a.x + b.x, a.y + b.y)
と使用するのが一般的。
ちなみに上記は必ず2つの引数を取る前提の書き方になるので、実際の引数は
def sample(x,y,*args)
(x,y以外に任意の数の引数を取る)
def sample2(**kwargs)
(任意の数の辞書形式の引数を取る)
→ ex. sample2(x=1, y=1) → {'x': 1, 'y': 1}
といったような形式で書かれることが多い。
なのでDjangoとかでたまにみかける@classmethod
もデコレータの一種だったということになる。
Pythonの組み込みのデコレータでクラス内でこのデコレータで修飾されたメソッドはクラスをインスタンス化せずとも、メソッドとして呼び出せるようになる。
具体的には以下のような例である。
参考: DjangoBrothers BLOG
【Python】@classmethod及びデコレータとは?
# まずはクラスの基本
class Person:
goal = "シーズン優勝"
count = 0
def __init__(self, first, last):
self.first = first
self.last = last
Person.count += 1
# インスタンス化
person1 = Person("井上", "明")
person2 = Person("八神", "太一")
print(person1.goal) # シーズン優勝
print(person2.goal) # シーズン優勝
# privateじゃないクラス変数は書き換えられる
Person.goal = "オールシーズン優勝"
print(person1.goal) # オールシーズン優勝
print(person2.goal) # オールシーズン優勝
# classmethodを使ってみる
class Person:
# 引数に*argsを使ってみる
def __init__(self, name, rank, *args):
self.name = name
self.rank = rank
self.score = args
# こちらは普通のメソッド
def sample1(self):
score = sum(self.score)
if not score:
return print("{0}のランクは{1}で今シーズンはまだ参加がありません".format(self.name,self.rank))
else:
return print("{0}のランクは{1}で今シーズンのスコアは{2}です".format(self.name,self.rank,score))
# クラスメソッド
@classmethod
def sample2(cls,str,*args):
name,rank = str.split(",")
score = sum(args)
return cls(name,rank,score)
# まずは普通にインスタンス化
test = Person("test","R")
print(test.name) # test
print(test.rank) # R
# メソッド使用
test.sample1() # testのランクはRで今シーズンはまだ参加がありません
# 今度はクラスメソッドでインスタンスを作る
test = Person.sample2("test,R",7,3,7)
# メソッドを使用、先程と結果が異なることを確認
test.sample1() # testのランクはRで今シーズンのスコアは17です
参考ページを元に書いてみましたが、こうやって基本に帰ったものを書くと腑に落ちました。
つまり、@classmethod
でデコレートしたメソッドは通常のメソッドのようにインスタンス化せずに直接ClassName.MethodName
の形で使用することができるが、それは @classmethod
でデコレートされたメソッドがクラスと引数からインスタンスを返してくれるからだったという至極当たり前なことだったわけです。
参考記事や私の書いた例はかなり単純かつ易しい例であると思いますが、要はインスタンス化ごとにある処理をして、それを変数にしてインスタンスを作るなんてことはやってられないので、クラス自体にその処理を関数化しておき、引数だけ与えればそれだけでインスタンスが生成されれば話は早いでしょうということですね。
で、その場合インスタンスが返ってきて欲しいので@classmethod
のデコレータを使う……ということの一例になるわけなのですね。勉強になります。
で、デコレータ本来の意味に戻ると@classmethod
はデコレートした関数(メソッド)にクラスメソッドの役割を持たせるように修飾するデコレータということになりますね。
最後に
Djangoで開発してるのにPythonのデコレータもわからないのか……と落胆もされてしまい、そもそもそれ以外の要因もおそらく多数あっただろうと思いますが、採用面接は見事に落ちてしまいましたが、基本的なところがやっぱりまだまだ足りないなということを痛感したので今回おさらいしてみました。
こういうところはどうしてもガッツリPythonやDjangoに集中して開発しないと中々身につきませんね……