Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
94
Help us understand the problem. What are the problem?

posted at

updated at

Pythonのデコレータってなんですか? と採用面接で聞かれて答えられなかった話

先日、面接の際にわかりますか? って聞かれてなんだっけ? となって結局わかりませんと答えてしまったので、情けなく感じたので復習します。
現役の方のツッコミや実践的な例等ご教授ありましたら是非コメント欄へお願いします。
すごく助かります。

参考:Pythonのデコレータを理解するための12Step

結論

デコレータは関数を引数にとって、さらに新たな関数を返す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に集中して開発しないと中々身につきませんね……

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
94
Help us understand the problem. What are the problem?