インタープリタのスタックフレームにアクセスし、メソッドの呼び出し元のオブジェクトを取得する

いまさらながらPythonにハマっています。Google App Engineが超楽しいですね。さて今日は、GAEではなくて、Pythonの言語機能を紹介します。環境はPython2.6.1、Mac OSX 10.6.1で試しました。

Pythonではinspectモジュールを使うことで、インタープリタのスタックフレームにアクセスすることができます。これを使って、Pythonで呼び出し元のオブジェクトを取得してみます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import inspect

def caller():
  try:
    # フレームレコードのリストを取得する
    framerecords = inspect.stack()

    # 二つ前のフレームレコードを取得する
    fremerecord = fremerecords[2] 

    # タプルからフレームを取得する
    frame = framerecord[0]

  # 指定したフレームの引数の情報を取得する
    arginfo = inspect.getargvalues(frame)

  # localsの辞書に格納されたselfオブジェクトを取得する
    return arginfo.locals['self'] if 'self' in arginfo.locals else None
  finally:
    del frame
  return None

class Hoge():
  def methodA(self):
    obj = caller()
    print obj # <__main__.Foo instance at 0x1d5620>
    obj.hoo()

class Foo():
  def __init__(self):
    self.hoge = Hoge()

  def foo(self):
    self.hoge.methodA()

  def hoo(self):
    print "hoo"

foo = Foo()
foo.foo()

caller()メソッドが、呼び出し元のオブジェクトを取得するメソッドです。それではソースコードをみてみます。

最初に呼び出し元スタックのフレームレコードのリストを取り出します。

    # フレームレコードのリストを取得する
    framerecords = inspect.stack()

callerメソッド内からみると取得したいスタックフレームレコードは2つ前のスタックフレームレコードなので、framerecords[2]でメソッドを呼び出したフレームレコードが取得できます。

    # 二つ前のフレームレコードを取得する
    fremerecord = fremerecords[2] 

このフレームレコードは、は長さ6のタプルで、このような構成になっています。

(フレームオブジェクト, ファイル名, 実行中の行番号, 関数名, コンテキストのソース行のリスト, ソース行リストの実行中行のインデックス)

フレームレコードからフレームオブジェクトを取得します。

    # タプルからフレームを取得する
    frame = framerecord[0]

フレームオブジェクトから、指定されたフレームに渡された引数の情報を取得します。

  # 指定したフレームの引数の情報を取得する
    arginfo = inspect.getargvalues(frame)

引数の情報は、長さ4のタプルで次の値を返します:
(引数, キーワード引数, 引数の名前, ローカル変数の辞書)

そこからローカル変数の辞書からselfと呼ばれているオブジェクトを取得すると、呼び出し元のオブジェクトが取得できます。

  # localsの辞書に格納されたselfオブジェクトを取得する
    return arginfo.locals['self'] if 'self' in arginfo.locals else None

最後に循環参照にならないようfinallyブロックでフレームを削除したら処理完了です。

実行結果はfoo->hoge->hooと呼び出しています。

使い道はあんまりなさそうだなあ。ダブルディスパッチをcalleeを渡さずに実行できるぐらいかも。
javaでできるとフレームワークの制限を打ち破れるので結構楽しいかも。