クラスデコレータ
クラスデコレータを使うと、クラスを最初に宣言するときに、メソッド、属性、プロパティ、基底クラスなどを追加/削除/変更する関数にクラスを渡して、そのクラスを変更できます。一連のクラス(必ずしもすべてが共通の基底クラスから派生しているとは限らない)に何らかの変更を適用する必要がある場合に、これが特に役立ちます。
次の例では、dump()関数がクラスデコレータとなり、入力クラスを受け取って、dump_methods()というメソッドをクラスに追加します。dump_methods()メソッドは自身のクラスの辞書を走査し、呼び出し可能なすべてのメソッドをタイトル付きで出力します。このメソッドがcls入力クラスを返すことに注意してください。後でこのデコレータクラスを使って、オブジェクトをインスタンス化します。
def dump(cls):
def dump_methods(self):
cls = type(self)
s = cls.__name__ + ' methods'
print (s + '\n' + '-' * len(s))
for k,v in cls.__dict__.items():
# The callable() builtin has been removed in Python 3.0
if hasattr(v, '__call__'):
print (k)
print ()
# Attch the dump_methods nested function as a method to the input class
cls.dump_methods = dump_methods
return cls
クラスデコレータを定義したところで、例として2つのクラスを修飾してみます。クラスAとクラスBはつまらない内容ですが、クラスデコレータの働きを示すには十分です。
@dump
class A:
def foo():
pass
def bar():
pass
@dump
class B:
def baz():
pass
クラスAとクラスBはどちらも@dumpで修飾されているので、次のようにそれぞれのインスタンスのdump_methods()を呼び出すことができます。
A().dump_methods() B().dump_methods() A methods --------- bar dump_methods foo B methods --------- dump_methods baz
ご覧のように、dump_methods()自体も出力に表示されています。デコレータで追加されたメソッドは、元のメソッドと区別が付かないからです。
クラスデコレータは、関数デコレータとメソッドデコレータの自然な延長線上にあります。Python 2.4の設計プロセス時(関数デコレータとメソッドデコレータが導入されたとき)には、クラスデコレータは重複機能のように思われました。「メタクラス」が非常によく似た機能を提供していたからです。主な違い(構文以外)は、メタクラスは継承されるという点です。つまり、あるクラスにメタクラスを適用すると、そのクラスのすべてのサブクラスがメタクラスを継承します。これに対し、クラスデコレータはサブクラスには影響を与えません。話がややこしくなりますが、メタクラスを1つだけ持つクラスデコレータを作成できます(それぞれ独自のメタクラスを持つ複数の基底クラスから継承するとややこしいことになりますが、結局は1つのメタクラスだけが勝ち残ります)。私は、デコレータの仕組みの方がメタクラスよりはるかに理解しやすく、クラス、関数、およびメソッドデコレータに統一された構文とセマンティクスがあるのは良いことだと思います。
詳細については、「PEP-3129」を参照ください。
関数アノテーション
関数アノテーションは(ABCと共に)私のお気に入りのPython 3000の新機能です。関数の引数や戻り値にPythonの任意の式を付けることができます。関数にどのようにアノテーションを付け、どのような意味を持たせるかは自分で決めます。型チェックは1つの代表例で、関数の各引数と戻り値に型のアノテーションを付けます。そうすることで、ユーザーはその関数を呼び出す方法を知ることができます。例えば、calc_circumference()関数が整数の半径を受け取り、浮動小数点数を返すとします。引数radiusと戻り値のアノテーション構文は次のようになります。
def calc_circumference(radius: int) -> float: return 2 * math.pi * radius
これ自体はそれほど大したことではありません。docstringで記述すれば済むことだからです(とはいえ、アノテーションの方が読みやすくなります)。関数アノテーションは、__anotations__という関数属性にdictとして格納されます。
>>> calc_circumference.__annotations__
{'radius': <class 'int'>, 'return': <class 'float'>}
もちろん、関数内のアノテーションにアクセスし、各引数の型がアノテーションと一致しているかどうかを確認することもできます。これは思ったより少し複雑です。Pythonでは、アノテーションが順不同のコレクションとしてdictに格納されるからです。次の簡単な関数は、3つのint引数を受け取り、それらを出力します。
def print_3(a: int, b: int, c: int)
次の例は、関数アノテーションにアクセスし、半径が実際に整数かどうかを検証する1つの方法を示しています。
def calc_circumference(radius: int) -> float: assert isinstance(radius, calc_circumference.__annotations__['radius']) return 2 * math.pi * radius
残念ながら、これはかなりできの悪いコードです。煩雑で間違いが起きやすく、たくさんの引数を確認する必要がある場合はとても読みにくくなります。しかも、このような関数固有の検証を記述する場合は、アノテーションは必要ありません。関数の作成者は「半径が整数でなければならないことを既に知っている」ので、次のように記述すれば済むからです。
assert isinstance(radius, int)
本領が発揮されるのは、汎用デコレータを使って関数アノテーションにアクセスするときです。call_checkデコレータ(リスト2を参照)を使うと、引数に型アノテーションが付いた関数やメソッドを修飾できます。このデコレータはアノテーションを抽出し、各引数の型が型アノテーションと一致していることを呼び出しごとにアサートします。また、戻り値の型も確認します(returnアノテーションが付いている場合)。辞書内のアノテーションは順不同なので、関数のコードオブジェクトから非キーワード引数の名前を抽出するには少し凝ったコードが必要です。要素に対してイテレーションを行うだけでは、関数の引数と順序が異なる場合があります。これが関数アノテーションの大きな弱点で、Python 3.1では修正されることを願っています。
def call_check(f):
def decorated(*args, **keywds):
# Get the code object for the function
c = f.__code__
# Names of arguments in order to lookup in the annotations dictionary
arg_names = c.co_varnames[:c.co_argcount]
annotations = f.__annotations__
# Collect all arguments into a single dict (now that all arguments have names)
d = {}
d.update(keywds)
d.update(zip(arg_names, args))
# Validate every argument
for name, value in d.items():
t = annotations[name]
if not isinstance(value, t):
s = 'The type of ' + name + '=' + str(value) + ' should be ' + t.__name__
raise Exception(s)
# Call the original function
result = f(*args, **keywds)
# Check return value
if 'return' in annotations:
assert isinstance(result, annotations['return'])
return result
return decorated
デコレータをテストするために、次のコードではcalc_circumferenceメソッドにデコレータを適用しています。
@call_check def calc_circumference(radius: int) -> float: return 2 * math.pi * radius
こうしておいて、有効な引数と無効な引数を使ってcalc_circumferenceを呼び出してみると、半径10を使った呼び出しでは正しい答えが返ることが分かります。無効な引数10.5(整数以外)を使った2番目の呼び出しでは例外が発生し、問題が発生した場所を示すメッセージと、値10.5を指定した引数radiusには整数を指定しなければならないことを知らせるメッセージが表示されます。
>>> calc_circumference(10)
62.8318530718
>>> calc_circumference(10.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "article_1.py", line 117, in decorated
raise Exception(s)
Exception: The type of radius=10.5 should be int
次のステップ(この部分は読者にお任せします)は、クラスデコレータまたはメタクラスを使って、クラスのすべてのメソッドをcall_checkデコレータで自動的に修飾することです。
関数アノテーションには、ヘルプ文字列、詳細な型情報(有効な値の範囲など)、直接検証関数、IDEを使いやすくするためのヒント、マッピング、アダプタ、対応するCの型など、ほかにもさまざまな用途があります。
関数アノテーションに関する1つの懸念は、これを使いすぎると「言語内の言語」を作ってしまい、アノテーションを処理するコードを見ないと、関数を呼び出したときにどう処理されるがわからなくなってしまうことです。
