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

  • 535
    Like
  • 0
    Comment
More than 1 year has passed since last update.

古い記事なんですが, Pythonのデコレータの概念をつかむには良記事だったので和訳意訳してみました.
http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/

Step1. 関数

>>> def foo():
...     return 1
...
>>> foo()
1

これは基本ですね. Pythonにおいて関数はdefキーワードにより関数名とパラメータのリスト(任意)とともに定義できます.また括弧付きで名前を指定することで実行できます.

Step2. スコープ

Pythonでは関数を作ると新しいスコープが作られます.言い方を変えるとそれぞれの関数がそれぞれに名前空間を持つということです.

Pythonではこれらを確認することのできる組み込み関数も用意されていて, locals()で自身の持つローカルな名前空間の値を辞書形式で返却します.

>>> def foo(arg):
...     x = 10
...     print locals()
...
>>> foo(20)
{'x': 10, 'arg': 20}

またグローバルな名前空間についてはglobals()で同様に確認ができます.

>>> y = 30
>>> globals()
{..., 'y': 30} #Pythonが自動的に作るグローバル変数が他にも表示されるが省略

Step3. 変数の解決規則

Pythonにおける変数の解決ルールは

  • 作成の際には常に新しい変数がその名前空間の中に作られる
  • 参照は同じ名前空間内を検索し, 無ければ外側に検索を広げていく

というものです.関数のローカルスコープでグローバルスコープの変数を参照してみましょう.

>>> text = "I am global!"
>>> def foo():
...     print text #1
...

>>> foo()
I am global!

関数の外側で定義したtextの内容がきちんと表示されていますね.#1ではまず関数内のローカル変数を探し, 無いので同じtextという名前のグローバル変数を探しにいっています.

では今度は, 関数の外側で定義した変数を, 関数内で変更してみましょう.

>>> text = "I am global!"
>>> def foo():
...     text = "I am local!" #2
...     print locals()
...
>>> foo()
{'text': 'I am local!'}
>>> text
'I am global!'

foo()が呼ばれた時にはtextの内容として関数内で代入した値がセットされていますが, 外側のグローバル変数のtextの値は変わっていません. #2では実際のところグローバル変数を探しに行かず, 関数foo内に新しいローカル変数textが作られているのです.

つまり関数の内側ではグローバル変数は参照はできるものの代入はできない, ということになります。

Step4. 変数のライフタイム

スコープだけでなくライフタイムについても知っておく必要があります.

>>> def foo():
...     x = 1
...
>>> foo()
>>> print x #1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

ここでエラーが起こっているのは, 上述のスコープの問題だけではありません. 名前空間は関数fooが呼ばれる都度作られ, 処理が終わると無くなってしまいます.つまり上の例では#1のタイミングでは文字通りxという変数はどこにも存在していない, ということです.

Step5. 関数の引数とパラメータ

Pythonでは関数に引数を与えることができます. 定義時のパラメータの名前が, ローカル変数の名前として使われます.

>>> def foo(x):
...     print locals()
>>> foo(1)
{'x': 1} # パラメータxがローカル変数名として使われている

Pythonでは関数にパラメータを設定したり, 呼び出し時に引数を与える様々な方法が提供されています. パラメータには必須パラメータと任意となるデフォルトパラメータがあります.

>>> def foo(x, y=0): # 1
...     return x - y
>>> foo(3, 1) # 2
2
>>> foo(3) # 3
3
>>> foo() # 4
Traceback (most recent call last):
  ...
TypeError: foo() takes at least 1 argument (0 given)
>>> foo(y=1, x=3) # 5
2

#1の例ではxがパラメータ, デフォルトの値を指定しているyがデフォルトパラメータになります.#2が通常の関数使用例ですが, #3のようにデフォルトパラメータについては省略することも可能です. 省略された引数はデフォルトの値(この場合0)を取ります.

またPythonについては名前付き引数も使えるので, 名前を指定することで順番を気にせず引数を指定することもできます(#4). Pythonにおける引数はただの辞書だと理解していれば納得のいく動きです.

Step6. 関数のネスト

Pythonでは関数内にネストして関数を定義できます.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

ルールはこれまで見てきたものと同じです. つまり#1ではローカル変数xを探すが見つからないので外側の名前空間を探し, outer内で定義されあているxを参照します.また#2ではinner()を呼び出していますが, ここで大切なのは innerというのも変数名の1つにすぎず, 解決規則に基づきouter内の名前空間内の定義を探して呼び出されている ということです.

Step7. 関数はPythonにおいてファーストクラスオブジェクトである

Pythonでは関数自身もまたオブジェクトにすぎません.

>>> issubclass(int, object) # Python内の全てのオブジェクトは同じ共通のクラスを継承して作られている
True
>>> def foo():
...     pass
>>> foo.__class__
<type 'function'>
>>> issubclass(foo.__class__, object)
True

これが何を意味するかというと, 関数を一般的な他の変数と何らかわりなく扱える - つまり他の関数の引数として与えたり, 関数の戻り値として使うことができるということです.

>>> def add(x, y):
...     return x + y
>>> def sub(x, y):
...     return x - y
>>> def apply(func, x, y):
...     return func(x, y)
>>> apply(add, 2, 1)
3
>>> apply(sub, 2, 1)
1

上の例では関数applyはパラメータとして指定された関数の実行結果を戻り値とするようになっていて, 関数add, subが引数として(他の変数と何ら変わりなく)与えられているのがわかります.

次の例はどうでしょうか.

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
...
>>> foo = outer() #2
>>> foo
<function inner at 0x...>
>>> foo()
Inside inner

上のapplyの例と異なるのは, #1で戻り値として実行結果ではなく関数そのものを指定しているということです.(inner()ではなくinnerを与えている)

これは#2のように普通に代入可能で, fooに関数が入っていて実行することもできることがわかります.

Step.8 クロージャ

上の例を少しだけ変えて見てみましょう.

>>> def outer():
...     x = 1
...     def inner():
...         print x
...     return inner
>>> foo = outer()
>>> foo.func_closure
(<cell at 0x...: int object at 0x...>,)

innerはouterによって返される関数で, fooに格納され, foo()により実行可能です...本当に?

Pythonの変数解決規則には完璧に従っていますが, ライフサイクルはどうなっていますか?変数xはouter関数が実行されている間のみ存在しています. ここではouter関数の処理が終わった後にinner関数がfooに代入されているので, foo()は実行できないのではないですか?

…この予想に反して, foo()は実行可能です。PythonがFunction clojures(クロージャ)の機能を持っているからで, これは グローバルスコープ以外で定義された関数(この場合inner)が, 「定義時」の自分を囲むスコープの情報を記憶している というものです. 実際に記憶されている事が上のようにfunc_closureプロパティを呼び出すことで確認できます.

「定義時」と書いたことを思い出してください. inner関数はouter関数が呼び出されるたびに新しく定義されています.

>>> def outer(x):
...     def inner():
...         print x
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

上の例ではprint1やprint2に直接値を引数として入れなくても, それぞれの内部のinner関数が何の値を出力すべきかを記憶しているのです. これを利用して固定の引数を取るようカスタマイズした関数を生成することもできるということですね.

Step.9 デコレータ

いよいよデコレータの話に入ります.
ここまでの話を踏まえて先に結論から言うと, デコレータとは「関数を引数に取り, 引き換えに新たな関数を返すcallable(*)」です.

>>> def outer(some_func):
...     def inner():
...         print "before some_func"
...         ret = some_func() #1
...         return ret + 1
...     return inner
>>> def foo():
...     return 1
>>> decorated = outer(foo) #2
>>> decorated()
before some_func
2

1つ1つ理解していきましょう.
ここでパラメータとしてsome_funcを取るouterという関数を定義しています. そしてouterの中でさらにinnerという内部関数を定義しています.
innerは文字列をprintした後, #1で返却する値を取得しています. some_funcはouterが呼び出されるごとに異なる値を取りえますが, ここではそれが何であれとりあえず実行(call)し, その実行結果に1を加えた値を返却します.
最後にouter関数はinner関数そのものを返却します.

#2ではsome_funcとしてfooを引数にouterを実行した戻り値を変数decoratedに格納しています. fooを実行すると1が返ってきますが, outerを被せたdecoratedではそれに1プラスされて2が返ってきます. 言ってみればdecoratedは, fooのデコレーション版(foo + 何かの処理)といえます.

実際に有用なデコレータを使う際には, decoratedのように別の変数を用意せずfooそのものを置き換えてしまうことが多いです. つまり下のように.

>>> foo = outer(foo)

もう元のfooは呼ばれず, 常にデコレーションされたfooが戻ることになりますね. もう少し実用的な例も見てみましょう.

とある座標のオブジェクトを保持するライブラリを使っているとします. そのオブジェクトはxとyの座標ペアを保持していますが, 残念ながら足し算や引き算など数字の処理機能を持っていません. しかし我々はこのライブラリを用いて大量の計算処理をする必要があり, ライブラリのソースを改編することも好ましくない状況だとします. どうすれば良いでしょうか?

アプローチとしては下のようにadd, subのような関数を作ってやればよいでしょう.

>>> class Coordinate(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return "Coord: " + str(self.__dict__)
>>> 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)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}

ここでたとえば「扱う座標系は0が下限である必要がある」といったチェック処理が必要だとしたらどうしますか?
つまり, 現状では

>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}

のようになりますが, sub(one, two)は(0, 0)を, add(one, three)は(100, 200)を返して欲しいということです. それぞれの関数に下限のチェックを入れていくということも考えられますが, ここでデコレータを使ってチェック処理を一元化してみましょう!

>>> 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 = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}

前の foo = outer(foo) とやっていることは変わりませんが, 有用なチェック機構をパラメータと関数の処理結果に対して適用することができています.

Step.10 @シンボルの適用

Pythonでは, デコレータの記述に関して@記号を用いたシンタックスシュガーが用意されています. すなわち

>>> add = wrapper(add)

という記述は,

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

という形で記述できます.

どうでしょう. ここまで読んで, classmethod や staticmethod のように有用なデコレータを自分で作ることはなかなかレベルが高い話ですが, 少なくともデコレータを使うのはそんなに難しくない - ただ @decoratorname を前置してやるだけだ! - と思っていただけると幸いです.

Step.11 *args と **kwargs

上で書いたデコレータwrapperは有用ですが, パラメータが2つという限られた関数にしか適用できません. 今回はそれでも良いのですが, もっとあらゆる関数に適用できるようなデコレータを書きたいと思ったらどうすれば良いでしょうか?
Pythonにはこれをサポートする機能も用意されています. 詳細は公式のドキュメントを読むのが良いですが, 関数を定義する際にパラメータに*(アスタリスク)を付けてやると任意の数の必須パラメータを受け付けることができます.

>>> def one(*args):
...     print args
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args):
...     print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)

任意のパラメータ部分に関してはリストで渡されているのがわかりますね. また定義時ではなく呼び出し時に引数に*を付けてやると, 既にリストやタプルになった引数をアンパックして固定引数に適用してくれます.

>>> def add(x, y):
...     return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) #1
3
>>> add(*lst) #2 <- #1と全く同じ意味を指します
3

また**(アスタリスク2個)という記法もあります. こちらはリストではなく辞書形式に対応しています.

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

**kwargsを関数定義に使うことは「全ての明示的に指定していないパラメータはkwargsという名前の辞書に格納される」ことを意味します. *argsと同じく関数呼び出し時のアンパックにも対応しています.

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     return x + y
>>> bar(**dct)
3

Step.12 ジェネリックなデコレータ

上の機能を用いて, 関数の引数をログに出力するデコレータを書いてみましょう. 簡略化のためログ出力はstdoutにprintしています.

>>> def logger(func):
...     def inner(*args, **kwargs): #1
...         print "Arguments were: %s, %s" % (args, kwargs)
...         return func(*args, **kwargs) #2
...     return inner

#1でinner関数は任意の個数, 形式のパラメータを取ることができ, #2でそれをアンパックして引数として渡すことができていることに注目してください. これによりどのような関数に対してもデコレータloggerを適用することができます.

>>> @logger
... def foo1(x, y=1):
...     return x * y
>>> @logger
... def foo2():
...     return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
20
>>> foo1(1)
Arguments were: (1,), {}
1
>>> foo2()
Arguments were: (), {}
2

より詳細が知りたい人のために

最後の例が理解できていれば, あなたはデコレータについて理解できたことになります! わからなければ前のステップに戻って何度でも読み返してみましょう.
もっと学習したい方はBruce Eckelの書いた下記素晴らしいエッセイを読むといいでしょう.

--

(*)callableとは引数を取って結果を返す, 呼び出し可能なオブジェクトの総称を指します. functionはもちろんclassやmethod, generatorなどもcallableです.