Pythonのデコレーターを簡単な12ステップで理解する
この資料執筆時では、約9年前の記事ですがわかりやすく説明しているため、とても有益です。
Python3 で動作させるため、コードは修正しています。
2012年7月1日
さて、私は冗談を言っているのかもしれません。Pythonのインストラクターとして、デコレーターの理解は、学生が最初に触れたときにいつも苦労するトピックです。それは、デコレータの理解が難しいからです。デコレーターを理解するには、いくつかの関数型プログラミングの概念を理解し、Pythonの関数定義と関数呼び出しの構文のユニークな機能を使いこなす必要があります。デコレーターを「使う」のは簡単です(セクション10参照)。しかし、その記述は複雑です。
デコレーターを簡単にすることはできませんが、パズルの各ピースを一歩ずつ進めていくことで、デコレーターをより自信を持って理解できるようになるかもしれません1。デコレーターは複雑なので、この記事は長くなりそうですが、お付き合いください。それぞれのピースをできるだけシンプルにすることを約束します。そして、それぞれのピースを理解すれば、デコレーターの仕組みを理解することができるでしょう 最低限のPythonの知識を前提としていますが、少なくともPythonに気軽に触れたことがある人には最も役立つ内容になるでしょう。 また、この記事ではPythonのdoctestモジュールを使ってPythonのコードサンプルを実行していることにも注目してください。コードはPythonのインタラクティブなコンソールセッションのように見えます(>>と...はPythonのステートメントを示し、出力は独立した行になっています)。時折、"doctest" で始まる不可解なコメントがありますが、これは doctest への単なる指示であり、安全に無視することができます。
1. 関数
Pythonの関数はdefキーワードで作成され、名前とオプションでパラメータのリストを受け取ります。関数は return キーワードで値を返すことができます。最も単純な関数を作って呼び出してみましょう。
code: python
>> def foo():
... return 1
>> foo()
1
関数の本体は(Pythonの他の複数行文と同様に)必須であり、インデントで示されます。関数名に括弧をつけることで、関数を呼び出すことができます。
2. スコープ
Pythonでは、関数は新しいスコープ(Scope) を作ります。Pythonistは、関数は自分の名前空間を持つと言うかもしれません。これは、Pythonが関数本体で変数名を見つけたときに、まず関数の名前空間を探すことを意味します。Pythonには自分の名前空間を見ることができるいくつかの関数があります。ローカルスコープとグローバルスコープの違いを調べるために、簡単な関数を書いてみましょう。
code: python
>> a_string = "This is a global variable"
>> def foo():
... print(locals())
>> print(globals()) # doctest: +ELLIPSIS
{..., 'a_string': 'This is a global variable'}
>> foo() # Point2
{}
組み込みの globals() 関数は、Python が知っているすべての変数名を含む辞書を返します。(分かりやすくするために、Pythonが自動的に作成するいくつかの変数は出力に含めていません。) Point2では、関数 foo() を呼び出し、関数内のローカル名前空間の内容を表示しています。見ての通り、関数 foo は独立した名前空間を持っており、現在は空です。
3. 変数解決のルール
もちろん、これは関数内でグローバル変数にアクセスできないことを意味するわけではありません。Pythonのスコープルールでは、変数の作成は常に新しいローカル変数を作成しますが、変数へのアクセス(修正を含む)はローカルスコープを調べてから、周囲のスコープをすべて検索して一致するものを探します。ですから、グローバル変数を表示するように関数 foo を変更すると、期待通りの動作になります。
code: python
>> a_string = "This is a global variable"
>> def foo():
... print(a_string) # Point1
>> foo()
This is a global variable
Point1の時点でPythonは関数内のローカル変数を探し、見つからなかった場合は同じ名前のグローバル変数を探します。
一方、関数の中でグローバル変数に代入しようとしても、これはできません。
code: python
>> a_string = "This is a global variable"
>> def foo():
... a_string = "test" # Point1
... print(locals())
>> foo()
{'a_string': 'test'}
>> a_string # Point2
'This is a global variable'
ご覧のように、グローバル変数はアクセス(変更可能なデータ型の場合は変更も)できますが、(デフォルトでは)代入することはできません。関数内のポイント1では、実際にグローバル変数と同じ名前の新しいローカル変数を作成しています。これを確認するには、関数 foo() の中のローカル名前空間を表示して、そこにエントリがあることを確認します。また、ポイント2でグローバル名前空間に戻って、変数a_stringの値をチェックすると、まったく変更されていないことがわかります。
4. 変数の寿命
変数は名前空間内に存在するだけでなく、寿命があることにも注意しましょう。考えてみましょう。
code: python
>> def foo():
... x = 1
>> foo()
>> print(x) # Point1
Traceback (most recent call last):
...
NameError: name 'x' is not defined
問題となるのはポイント1のスコープルールだけではありません(だからこそNameErrorが発生するのですが)、Pythonや他の多くの言語で関数呼び出しがどのように実装されているかにも関係します。この時点では、変数 x の値を取得するために使用できる構文はありません - 文字通り存在しないのです! 関数 foo() のために作られた名前空間は、関数が呼び出されるたびにゼロから作成され、関数が終了すると破棄されます。
5. 関数の引数とパラメータ
Pythonでは、関数に引数を渡すことができます。パラメータ名は関数内のローカル変数になります。
code: python
>> def foo(x):
... print(locals())
>> foo(1)
{'x': 1}
Pythonには、関数のパラメータを定義したり、関数に引数を渡したりする様々な方法があります。詳しい説明は、Pythonの関数の定義に関するドキュメントをご覧ください。ここでは簡単に説明しますと、関数のパラメータには、必須の位置パラメータと、任意の名前付きデフォルト値パラメータがあります。
code: python
>> def foo(x, y=0): # Point1
... return x - y
>> foo(3, 1) # Pont2
2
>> foo(3) # Point3
3
>> foo() # Point4
Traceback (most recent call last):
...
TypeError: foo() takes at least 1 argument (0 given)
>> foo(y=1, x=3) # Point5
2
ポイント1では、1つの位置パラメータ x と1つの名前付きパラメータ y を持つ関数を定義しています。ポイント2で見られるように、この関数は普通に引数を渡して呼び出すことができます。関数定義で名前付きパラメータとして定義されているにもかかわらず、fooのパラメータには位置によって値が渡されます。Python は名前付きパラメータ y の値を受け取らなかった場合、宣言したデフォルト値 0 を使用します。もちろん、最初の (必須、位置指定) パラメータの値を省略することはできません。
すべてが明確でわかりやすいですね。Pythonは関数呼び出し時に名前付き引数をサポートしていますが、ここで少し混乱してきます。ポイント#5を見てください。ここでは、関数が1つの名前付きパラメータと1つの位置指定パラメータで定義されているにもかかわらず、2つの名前付き引数で関数を呼び出しています。パラメータに名前があるので、渡す順番は問題ではありません。
もちろん、その逆のケースもあります。ポイント2 の foo(3,1) の呼び出しでは、名前付きパラメータとして定義されているにもかかわらず、順序付きパラメータ x の引数に 3 を渡し、2 番目のパラメータに 2 番目の引数 (整数の 1) を渡しています。
ふーっ! 「関数のパラメータには名前と位置がある」という非常にシンプルな概念を説明するのに、たくさんの言葉を使ったように感じます。関数のパラメータには名前と位置があるということです。これは、関数の定義時か呼び出し時かによって微妙に異なる意味を持ち、位置パラメータのみで定義された関数に名前付き引数を使用することも、その逆も可能です。繰り返しになりますが、急ぎすぎた場合は、必ずドキュメントをチェックしてください。
6. 関数の入れ子
Pythonでは、入れ子になった関数を作ることができます。つまり、関数の中に関数を宣言することができ、すべてのスコーピングとライフタイムのルールは通常通り適用されます。
code: python
>> def outer():
... x = 1
... def inner():
... print(x) # Point1
... inner() # Point2
...
>> outer()
1
これは少し複雑に見えますが、それでもかなり賢明な方法で動作しています。Python は x という名前のローカル変数を探しますが、失敗したので別の関数である周囲のスコープを探しています。変数 x は outer() 関数のローカル変数ですが、先ほどと同様に inner() 関数は エンクロージングスコープへのアクセス権を持っています (少なくとも読み取りと変更のアクセス権)。2の時点で、内側の関数を呼び出します。ここで重要なのは、innerはPythonの変数検索ルールに従った単なる変数名であるということです。Pythonはまずouterのスコープを探し、innerという名前のローカル変数を見つけます。
訳注: Python のスコープには LEGB ルールと呼ばれる規則があります。
● ローカルスコープ(Local Scope):ローカル変数
● エンクロージャスコープ(Enclosure Scope):関数の外側のローカル変数
● グローバルスコープ(Global Scope):グローバル変数
● 組み込みスコープ(Built-in Scope):ビルトイン変数、__name__ など未定義でも、どこからでも使用できる変数
この頭文字をとってLEGBルールと言われます。ルールというのは、この説明順に上から下に優先度が低くなります。
つまり、同じ名前の変数名がローカルとグローバルにあれば、ロケールスコープの値が採用されるというわけです。
7. Pythonでは関数はファーストクラスオブジェクトである
これは単純に、Pythonでは関数は他のものと同様にオブジェクトであるという観察です。ああ、変数を含む関数、あなたはそれほど特別ではありません。
code: python
>> issubclass(int, object) # all objects in Python inherit from a common baseclass
True
>> def foo():
... pass
>> foo.__class__ # 1
<type 'function'>
>> issubclass(foo.__class__, object)
True
関数に属性があるとは考えたことがないかもしれませんが、関数は他のものと同様にPythonのオブジェクトです。(しかし、関数はPythonのオブジェクトであり、他のすべてのものと同じです。) おそらくこれは学術的な方法で指摘しているのだと思いますが、Pythonでは関数は他の種類の値と同じように通常の値に過ぎません。つまり、関数に関数を引数として渡したり、関数から関数を戻り値として返すことができるのです。もしこのようなことを考えたことがないのであれば、次のような完全に合法なPythonを考えてみてください。
code: python
>> def add(x, y):
... return x + y
>> def sub(x, y):
... return x - y
>> def apply(func, x, y): # Poiint1
... return func(x, y) # Point2
>> apply(add, 2, 1) # Point3
3
>> apply(sub, 2, 1)
1
add() と sub() は、2つの値を受け取り、計算された値を返す、Python の普通の関数ですから、この例はあまり違和感がないかもしれません。ポイント1の時点では、関数を受け取るための変数は、他の変数と同様に普通の変数であることがわかります。ポイント2では apply() に渡された関数を呼び出しています。Pythonの括弧は呼び出し演算子で、変数名に含まれる値を呼び出します。そしてポイント3では、関数を値として渡すことはPythonでは特別な構文ではなく、関数名は他の変数と同様に単なる変数ラベルであることがわかります。
このような動作は以前にも見たことがあるかもしれません。Pythonは、キーパラメータに関数を与えることでソート組み込み関数をカスタマイズするような、よく使われる操作の引数として関数を使用します。しかし、関数を値として返す場合はどうでしょうか?考えてみてください。
code: python
>> def outer():
... def inner():
... print("Inside inner")
... return inner # Point1
...
>> foo = outer() # Point2
>> foo # doctest:+ELLIPSIS
<function inner at 0x...>
>> foo()
Inside inner
これは、少し奇妙に見えるかもしれません。ポイント1では、変数innerを返していますが、これはたまたま関数のラベルでした。ここには特別な構文はありません。私たちの関数は、そうでなければ呼び出すことができない内側の関数を返しているのです。変数寿命を覚えていますか?inner()関数はouter() 関数が呼ばれるたびに新しく再定義されますが、もしinnerが関数から返されなかったら、単にスコープ外に出たときに存在しなくなってしまいます。
ポイント2では、関数inner()の戻り値をキャッチして、新しい変数fooに格納することができます。foo を評価すると、それは本当に関数 inner() を含んでいて、call 演算子 (括弧(())、覚えていますか?) を使ってそれを呼び出すことができることがわかります。これは少し変に見えるかもしれませんが、ここまでは特に難しいことはありませんよね?ちょっと待ってください、これから変な方向に進んでいきますよ
8. クロージャ
まずは定義からではなく、別のコードサンプルから見てみましょう。前回の例に少し手を加えてみましょう。
code: python
>> def outer():
... x = 1
... def inner():
... print(x) # Point1
... return inner
>> foo = outer()
>> foo.func_closure # doctest: +ELLIPSIS
(<cell at 0x...: int object at 0x...>,)
先ほどの例から、inner()はouter()が返す関数で、fooという変数に格納されており、foo()で呼び出すことができることがわかりました。しかし、それは実行されるのでしょうか?まず、スコーピングルールについて考えてみましょう。
すべてはPythonのスコーピングルールに従って動作します。x は関数 outer() のローカル変数です。
しかし、変数の寿命の観点からはどうでしょうか。変数 x は関数 outer() に対してローカルであり、関数 outer() が実行されている間だけ存在することになります。outer() が戻ってくるまで inner() を呼び出すことができないので、Pythonの動作モデルによれば、inner() を呼び出す頃には x はもう存在しないはずで、おそらく何らかのランタイムエラーが発生するはずです。
しかし、私たちの予想に反して、返された inner() 関数は動作することがわかりました。Pythonは関数クロージャと呼ばれる機能をサポートしています。これは、非グローバルスコープで定義された内部関数が、定義時にその周囲の名前空間がどうなっていたかを記憶することを意味します。これは、エンクロージャスコープの変数を含むinner()関数の func_closure 属性を見ればわかります。
関数 outer() が呼び出されるたびに、関数 inner() が新たに定義されることを覚えておいてください。今のところ x の値は変化していないので、戻ってきた各内側の関数は別の内側の関数と同じことをしていますが、これに少し手を加えてみたらどうでしょう?
code: python
>> def outer(x):
... def inner():
... print x # 1
... return inner
>> print1 = outer(1)
>> print2 = outer(2)
>> print1()
1
>> print2()
2
この例から、クロージャ(関数が自分の囲んだ範囲を記憶するという事実)を利用して、基本的にハードコードされた引数を持つカスタム関数を作ることができることがわかります。内側の関数に1や2という数字を渡しているのではなく、どの数字を表示すべきかを「記憶」している内側の関数のカスタムバージョンを作っているのです。
これだけでも強力なテクニックです。ある意味、オブジェクト指向のテクニックに似ていると思うかもしれません。外側の関数は内部関数のコンストラクタで、x はプライベートなメンバー変数のように動作します。Pythonのsorted()関数のkeyパラメータを知っている人なら、リストのリストを最初の項目ではなく2番目の項目でソートするラムダ関数を書いたことがあるでしょう。これで、取得したいインデックスを受け取り、keyパラメータに渡すのに適した関数を返すitemgetter()関数を書けるようになったかもしれません。
しかし、クロージャを使ってそんなありふれたことをするのはやめましょう。代わりに、もう一伸びしてデコレーターを書いてみましょう!
9. デコレーター
デコレーターとは、関数を引数に取り、代わりの関数を返す単なる呼び出し可能なものです。簡単なところから始めて、便利なデコレーターを作っていきましょう。
code: python
>> def outer(some_func):
... def inner():
... print("before some_func")
... ret = some_func() # Point1
... return ret + 1
... return inner
>> def foo():
... return 1
>> decorated = outer(foo) # Point2
>> decorated()
before some_func
2
デコレーターの例をよく見てみましょう。outer() という名前の関数を定義しました。この関数には 1 つのパラメータ some_func があります。outer() の中に inner() という名前のネストした関数を定義しています。inner() 関数は、文字列を表示してから some_func を呼び出し、その戻り値をポイント1 でキャッチします。some_func の値は outer() が呼ばれるたびに異なるかもしれませんが、どのような関数であってもそれを呼び出します。最終的にinner()はsome_func()の戻り値+1を返します。そして、decoratedに格納されている戻り値の関数をポイント2で呼び出すと、print()の結果が得られ、fooを呼び出したときに得られると思われる本来の戻り値1ではなく、2の戻り値が得られることがわかります。
この変数 decorated は、foo のデコレーター版と言えます。実際、もし便利なデコレーターを書いたら、fooをデコレーター版に置き換えて、常に「プラスアルファ」のfooを得られるようにしたいと思うかもしれません。新しい構文を覚えることなく、関数を格納した変数を再割り当てするだけでそれが可能になります。
code: python
>> foo = outer(foo)
>> foo # doctest: +ELLIPSIS
<function inner at 0x...>
これで、foo() を呼び出すと、オリジナルのfooではなく、デコレートしたバージョンの foo が呼び出されることになります。お分かりいただけたでしょうか?もっと便利なデコレーターを書いてみましょう。
座標オブジェクトを提供するライブラリがあったとします。座標オブジェクトは主に x と y の座標ペアで構成されているとします。残念なことに、この座標オブジェクトは数学的な演算子をサポートしておらず、ソースを変更することもできないので、自分でこのサポートを追加することもできません。しかし、私たちはたくさんの数学を行うことになるので、2つの座標オブジェクトを受け取り、適切な数学的処理を行うadd()関数とsub()関数を作りたいと思います。これらの関数は簡単に書くことができます(説明のために座標クラスのサンプルを用意します)。
code: python
>> 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}
しかし、足し算や引き算の関数が、何らかの境界チェックの動作をしなければならないとしたらどうでしょうか。例えば、正の座標に基づいてのみ加算や減算ができ、結果も正の座標に限定されるべきでしょう。そこで、現在
code: python
>> 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}
となりますが、むしろ1と2の差を{x: 0, y: 1} と2の差は{x: 0, y: 0} とし、1と3の和は {x: 100, y: 200} とする方が、1も2も3も修正せずに済む。各関数の入力引数や戻り値に境界チェックを追加する代わりに、境界チェックのデコレーターを書いてみましょう!
code: python
>> def wrapper(func):
... def checker(a, b): # 1
... 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}
このデコレータは先ほどと同じように動作し、関数の修正版を返しますが、今回は入力パラメータと戻り値をチェックして正規化し、負のxやyの値には0を代入するという便利な機能を持っています。
境界チェックを独自の関数に分離し、デコレータでラップして気になるすべての関数に適用することで、このようにすることがコードをすっきりさせるかどうかは意見が分かれるところです。代わりの方法としては、それぞれの数学関数の中で、各入力引数と結果の出力を返す前に関数を呼び出すことになりますが、少なくとも関数に境界チェックを適用するために必要なコードの量という点では、デコレータを使った方が繰り返しが少ないことは否定できません。実際、もし自分たちの関数をデコレートするのであれば、デコレーターの適用をもう少し明らかにすることができるでしょう。
10. シンボルは関数にデコレータを適用します
Python 2.4 では、関数定義の前にデコレータ名と @ シンボルを付けることで、関数をデコレータで包むことができます。上のコードサンプルでは、関数を含む変数をラップしたものに置き換えることで、関数をデコレートしています。
code: python
>> add = wrapper(add)
このパターンは、任意の関数をラップするために、いつでも使用することができます。しかし、関数を定義している場合は、次のように@記号で「装飾」することができます。
code: python
>> @wrapper
... def add(a, b):
... return Coordinate(a.x + b.x, a.y + b.y)
重要なのは、これは単に元の変数 add をラッパー関数からの戻り値に置き換えたのと同じで、Python は何が起こっているのかを明確にするために構文上の工夫をしているだけだということです。
繰り返しになりますが、デコレーターを使うのは簡単です。staticmethodやclassmethodのような便利なデコレータを書くのは難しいとしても、それらを使うには、関数の前に@decoratornameを付けるだけです。
11. 可変引数: *argsと**kwargs
便利なデコレータを書きましたが、特定の種類の関数(2つの引数を取るもの)にしか使えないようにハードコードされています。内側の関数チェッカーは2つの引数を受け取り、その引数をクロージャでキャプチャされた関数に渡します。では、どんな関数にも対応できるデコレータが欲しいとしたらどうでしょう?装飾された関数を変更することなく、すべての装飾された関数の関数呼び出しごとにカウンタをインクリメントするデコレータを書いてみましょう。つまり、デコレータはデコレートされた関数の呼び出しシグネチャを受け入れなければならず、また、デコレートされた関数を引数に渡して呼び出さなければなりません。
訳注: シグネチャ(signature)
シグネチャ(signature)は言葉としては「署名」の意味になります。シグニチャと表記されることもあります。
プログラミング言語(特にオブジェクト指向言語)では、シグネチャは一意であることを示すためのものを言います。
通常、シグネチャには、「メソッド/関数名」「パラメータ数と順序 、パラメータの型」、「戻り値の型」が含まれます。
Python では、inspect.signature() で関数のシグネチャを調べることができます。
たまたま、Pythonはこの機能を構文的にサポートしています。詳細はPythonチュートリアル を読んでください。しかし、関数を定義するときに使用される* 演算子は、関数に渡された追加の位置引数は、*で始まる変数に格納されることを意味します。つまり code: python
>> def one(*args):
... print(args) # Point1
>> one()
()
>> one(1, 2, 3)
(1, 2, 3)
>> def two(x, y, *args): # Point2
... print(x, y, args)
>> two('a', 'b', 'c')
a b ('c',)
最初の関数は、位置引数が渡された場合、それを単純に表示します。Point1にあるように、関数内では単に変数argsを参照しています。*args は、位置引数が変数 args に格納されるべきであることを示すために、関数定義でのみ使用されます。Pythonでは、ポイント2で見られるように、いくつかの変数を指定したり、追加のパラメータを args に格納することもできます。
* 演算子は、関数を呼び出す際にも使用でき、ここでは類似の意味を持ちます。関数を呼び出す際に変数の前に * を付けると、その変数の内容を抽出して位置引数として使用することを意味します。もう一度例を挙げます。
code: python
>> def add(x, y):
... return x + y
>> add(lst0, lst1) # Point1 3
>> add(*lst) # Point2
3
ポイント1のコードはポイント2のコードと全く同じことをしています。つまり、手動でできることをポイント2でPythonが自動的にやってくれているのです。これは悪いことではありません。 *args は、関数を呼び出すときに反復可能な変数から位置変数を抽出するか、関数を定義するときに余分な位置変数を受け入れることを意味します。
辞書やキーと値のペアに対して * が反復可能な位置変数に対して行っていることと同じことを行う ** を導入すると、物事は少しだけ複雑になります。簡単ですよね?
code: python
>> def foo(**kwargs):
... print(kwargs)
>> foo()
{}
>> foo(x=1, y=2)
{'y': 2, 'x': 1}
関数を定義する際に **kwargs を使用して、キャプチャされていないすべてのキーワード引数を kwargs と呼ばれる辞書に格納することを示すことができます。先に述べたように、args や kwargs という名前はPythonの構文の一部ではありませんが、関数を宣言する際にこれらの変数名を使用することは慣習となっています。ちょうど * のように、関数を定義するときだけでなく、関数を呼び出すときにも ** を使うことができます。
code: python
>> dct = {'x': 1, 'y': 2}
>> def bar(x, y):
... return x + y
>> bar(**dct)
3
12. より汎用的なデコレーター
新しい力を手に入れたので、関数の引数を「記録」するデコレータを書くことができます。ここでは、単純化のために標準出力に出力することにします。
code: python
>> def logger(func):
... def inner(*args, **kwargs): # Point1
... print(f'Arguments were: {args}, {kwargs}')
... return func(*args, **kwargs) # Point2
... return inner
内側の関数は、ポイント1で任意の数とタイプのパラメータを受け取り、ポイント2でラップされた関数の引数として渡していることに注目してください。これにより、シグネチャに関係なく、どんな関数でもラップしたりデコレートしたりすることができます。
code: python
>> @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
関数を呼び出すと、各関数の期待される戻り値の他に、"Arguments were:" という出力行が表示されます。
デコレーターの詳細
先ほどの例でデコレーターについて理解できたと思います。おめでとうございます。この新しい力を活用してください。
もう少し勉強してみるのもいいでしょう。Bruce Eckel氏はデコレータに関する素晴らしいエッセイ(日本語訳: Pythonデコレーター 1:Python デコレーターの紹介) を書いており、Pythonで関数の代わりにオブジェクトを使ってデコレータを実装しています。私たちの純粋な関数型のコードよりも、OOP型のコードの方が読みやすいかもしれません。Bruceはまた、デコレータへの引数の提供に関するフォローアップのエッセイ(日本語訳:Pythonデコレーター 2:引数を持つデコレーター) を書いていますが、これも関数よりもオブジェクトで実装する方が簡単かもしれません。最後に - ビルトインの functools wraps 関数についても調べてみてください。これは(紛らわしいことですが)デコレータで使用できるデコレータで、置換関数のシグネチャを変更して、装飾された関数のように見せることができます。