Python で特定のクラスに対してそのサブクラスを取得する(+ファイル名取得)

もっと簡単な方法があるかも、ということでさらしてみる。


PythonDSL 的なものを書くときに*1

class OutputFoo(OutputBase):
  prop1 = "プロパティとか"
  def event1(self):
    # なんか処理
    pass

みたいのを書かせて、OutputFoo に対して処理をする、というやりかたを思いつくとする。


OutputFoo を含むスクリプトを execfile() で読み込ませる場合、読み込み側は OutputFoo の名前すら知らないし、execfile() 抜けたときには OutputFoo とその実体の間のバインドがはずれてるので、そもそも直接参照することすら出来ない。

DSL 側で登録メソッドを呼ぶ、というのが一番簡単だが、「これ、DRY じゃないよね」という上から目線の批判にいらいらする。


という場合に良い方法はないだろうかと思って、あれこれ試した中ではこれが一番短く書けるかな、というコード。

class SubclassListener:
  def __init__(cls, name, bases, dict):
    if hasattr(cls, "subclasses"):
      cls.subclasses.append(cls)
    else:
      cls.subclasses = []


class OutputBase:
  __metaclass__ = SubclassListener

class OutputFoo(OutputBase):
  pass
class OutputBar(OutputBase):
  pass

for cls in OutputBase.subclasses:
  print cls.__name__
>>> OutputFoo
>>> OutputBar


"__metaclass__ = SubclassListener" を書くだけで、そのクラスに対して定義されたサブクラスは <クラス>.subclasses に追加される。
ベースクラスを定義したときにも SubclassListener は呼ばれるけど、初回呼び出し時は subclasses アトリビュートが存在しないので、ちょうどそのときに入れ物を用意させている。

追記

やっぱりあった。

id:mopemope さん『何もしなくてもOutputBase.__subclasses__()でいけるはずです』

ありがとうございます。できました orz


手元ではさらに細工して、OutputFoo を記述したスクリプトファイル名も取得できるようにしているのだけど、それも metaclass なんか使わずにできるのかなあ。

http://d.hatena.ne.jp/tokuhirom/20041004/p13 経由で

def _functionId(nFramesUp):
    """ Create a string naming the function n frames up on the stack.
    """
    co = sys._getframe(nFramesUp+1).f_code
    return "%s (%s @ %d)" % (co.co_name, co.co_filename, co.co_firstlineno)

def notYetImplemented():
    """ Call this function to indicate that a method isn't implemented yet.
    """
    raise Exception("Not yet implemented: %s" % _functionId(1))

#...

def complicatedFunctionFromTheFuture():
    notYetImplemented()

こんなんみつけたけど、この系統の手法だと DSL 側に記述が必要。

追記

コメントで id:mopemope さんに、親クラスの __init__ に sys._getframe() 書けばサブクラスが定義されてるファイル名も取れるんじゃない? と示唆をいただいて試してみました。


が、そもそも execfile() で呼び出した場合はそのファイル名が frame に積まれない様子。

# loader.py
execfile("execfile.py")

# execfile.py
import sys
print -1, sys._getframe(-1).f_globals['__file__']
print 0, sys._getframe(0).f_globals['__file__']
print 1, sys._getframe(1).f_globals['__file__']
print 2, sys._getframe(2).f_globals['__file__']
$ python loader.py
-1 loader.py
0 loader.py
1 loader.py
2
Traceback (most recent call last):
  File "loader.PY", line 2, in <module>
    execfile("execfile.py")
  File "execfile.py", line 7, in <module>
    print 2, sys._getframe(2).f_globals['__file__']
ValueError: call stack is not deep enough

execfile() にこだわっている理由は、DSL 側で import を書かなくていいから。
って、そうか、そりゃ frame に積まれるわけないわな。読み込んで eval してんのと一緒やもんな。


ちなみに、今は metaclass を使ってこんな感じに実現している。

class LoadListener(type):
  current_filename = ''
  def __init__(cls, name, bases, dict):
    cls.filename = LoadListener.current_filename

def load(filename):
  LoadListener.current_filename = filename
  execfile(filename)

class OutputBase:
  __metaclass__ = LoadListener


load("sample.py") # class OutputFoo(Output) が定義されている

print Output.__subclasses__[0]    # >> <class OutputFoo>
print Output.__subclasses__[0].filename    # >> "sample.py"

かっこうわるいが、とりあえず実現できているのでよしとしている。

*1:そもそも PythonDSL 書くな、というのが最も説得力あり