2.4への機能強化で広がるPythonの世界UNIX USER2005年2月号特別企画より転載(3/4 ページ)

» 2005年01月24日 16時00分 公開
[磯 蘭水,UNIX USER]

関数やメソッドの修飾構文

 関数やメソッドの修飾構文の機能向上は、Python 2.4の拡張のうち、目玉ともいえる部分です。この構文については開発者メーリングリストにおいて、長期間にわたって議論が繰り返されました。関数やメソッドに対する修飾機能そのものはPython 2.2から存在していましたが、専用の構文は用意されていませんでした。

 たとえば、クラスメソッドを定義する場合、従来はリスト1のように記述していました。この方法はクラスの記述が複雑になったり、メソッドのコード量が増えると、メソッドの定義部分とクラスメソッドとしての登録部分が離れてしまい、可読性が低下する問題がありました。

リスト1 クラスメソッドを定義する場合の従来の記述例
class foo:
   def method(args):
       :
       :
   method = classmethod(method)

 Python 2.4では、「@」という記号を使って、メソッドを修飾する構文が提供され、リスト2のように書くことが可能になりました。この@を使った構文は、クラスメソッドを定義する場合以外にも、自由に使用できます。

リスト2 Python 2.4では、「@」という記号を使って、メソッドを修飾する構文が提供されている
class foo:
   @classmethod
   def method(args):
       :
       :

 実行例12では、関数オブジェクトにcreatedTimeという属性を作成し、その値として、時刻を代入する修飾用の関数createdTimeを定義しています。関数fooとbarを定義する際にこの修飾を指定しています。結果として、それぞれの関数オブジェクトにはcreatedTimeという属性が作られ、それを参照すれば定義された時刻が分かります。このような関数を修飾する関数のことをデコレータといいます。

実行例12 関数オブジェクトにcreatedTimeという属性を作成し、その値として時刻を代入する修飾用の関数createdTimeを定義している
>>> import time
>>> def createdTime(funcObj):
...     funcObj.createdTime = time.time()
...     return funcObj
...
>>> @createdTime
... def foo(x):
...     print x + 1
...
>>> @createdTime
... def bar(name):
...     print "Hello %s" % name
...
>>> foo.createdTime
1103025973.760793
>>> bar.createdTime
1103026042.8535249
>>> foo(10)
11
>>> bar("World")
Hello World

 ここで重要なのは、修飾用の関数が適用されるのは実行時ではなく、定義時である点です。つまり、デコレータによって複数の関数に共通したシグネチャを持たせる操作を簡単に記述できるようになったわけです。

 デコレータには引数を与えることもでき、これを利用するといろいろと面白いことができます。リスト3は関数の引数の型をチェックするデコレータの例です。実行結果は実行例13のようになり、正しく引数の型チェックが行われていることが分かります。

リスト3 関数の引数の型をチェックするデコレータの例
赤字の部分にマウスカーソルをあてると内容を確認できます

import math

def declareArgs(*argTypes):
 def checkArguments(func):
  assert func.func_code.co_argcount == len(argTypes)
  def wrapper(*args, **kwargs):
   pos = 1
   for (arg, argType) in zip(args, argTypes):
    assert isinstance(arg, argType), \
       "Value %r dose not match %s at %d" % (arg, argType, pos)
    pos += 1
   return func(*args, **kwargs)

  wrapper.func_name = func.func_name
  return wrapper

 return checkArguments


@declareArgs(float, float)
def calcDistance(x, y):
 return math.sqrt(x * x + y * y)

print calcDistance(3.14, 1.592)
print calcDistance(2.0, 4)



実行例13 リスト3の実行結果。正しく引数の型チェックが行われていることが分かる
$ /opt/Python-2.4/bin/python DecoratorTest.py
3.52052041607
Traceback (most recent call last):
  File "DecoratorTest.py", line 27, in ?
    print calcDistance(2.0, 4)
  File "DecoratorTest.py", line 11, in wrapper
    assert isinstance(arg, argType), \
AssertionError: Value 4 dose not match at 2

 また、この一連の動作はリスト4のようにコードを書いた場合とまったく同じです。リスト4で、print calcDistance(3.14, 1.592)を実行すると、実際に起動する関数本体はwrapper(*args, **kwargs)ということになります。wrapper関数の引数argsは(3.14, 1.592)になっています。この実引数がargTypesで与えられている型に対応しているかを順番にチェックし、すべて正しければ func(*args, **kwargs)が実行され、結果を返します。変数funcは、上位の関数checkArgumentsでオリジナルのcalcDistanceオブジェクトが代入されていますので、結果としてオリジナルのcalcDistance(3.14, 1.592)が実行されることになります。

リスト4 リスト3と同様の動作をするコード
def calcDistance(x, y):
    return math.sqrt(x * x + y * y)

checkArgFuncObj = declareArgs(float, float)
wrapperFuncObj = checkArgFuncObj(calcDistance)
calcDistance = wrapperFuncObj

 このような関数のカスタマイズは、従来も行えましたが、デコレータを使うことでさらに簡潔に記述できるようになったわけです。デコレータはこれ以外にも多くの利用法がありますので、研究してみると面白いと思います。

10進数演算モジュール

 Python 2.4から、10進数演算の機能を提供するDecimalモジュールが標準になりました。これで、1.1を10回足すと正しく11になる計算ができるようになりました(リスト5)。浮動小数点数による計算では、丸め誤差のため計算結果は11.0になりませんが、decimalを使った10進数計算では正しい結果が得られます。

リスト5 10進数演算の機能を提供するDecimalモジュールを利用すると、浮動小数点数を含む数値の10進数計算で正しい結果が得られる
>>> import decimal
>>> dnum = decimal.Decimal("1.1")
>>> dsum = decimal.Decimal("0")
>>> rnum = 1.1
>>> rsum = 0.0
>>> for i in range(10):
...   dsum = dsum + dnum
...   rsum = rsum + rnum
...
>>> rsum
10.999999999999998
>>> dsum
Decimal("11.0")
>>> str(dsum)
'11.0'

Copyright(c)2010 SOFTBANK Creative Inc. All rights reserved.

注目のテーマ