自分が触れるコード中の親クラスのファイルパスを列挙する
親クラスのファイルのパスのリストが欲しい
Pythonを書いているときに、あるクラスHoge
を継承しているクラスFuga
と、Fuga
をさらに継承しているPiyo
について、それらのクラス定義が存在しているファイルのパスが欲しくなった(キャッシュ機構の実装中に)。
例えば、以下のようなクラスたちを考える。
class Hoge:
...
from hoge import Hoge
class Fuga(Hoge):
...
from fuga import Fuga
class Piyo(Fuga):
...
ディレクトリ構成は以下。
src
├── hoge.py
├── fuga.py
└── piyo.py
ここで、解きたいのはPiyo
からファイルパスのハードコードなしでsrc/hoge.py
とsrc/fuga.py
を得るにはどうすればよいか?という問題。
TL;DR
こういうクラスメソッドを用意すればいい。
class Hoge:
@classmethod
def get_class_paths_in_my_project(cls):
# specify the root directory of our project
src_dir = Path.cwd() / "src"
ret = []
# lists classes with Method Resolution Order (MRO)
for c in cls.mro():
m = sys.modules[c.__module__]
try:
path = Path(m.__file__)
except AttributeError: # this module is `builtins` module
continue
try:
path.relative_to(src_dir)
except ValueError: # not in src dir
continue
ret.append(path)
return ret
解決策
まず、あるクラスの親クラスを列挙するには、以下のようにクラスメソッドmro
を使えばいい(mroはMethod Resolution Orderのこと)。
class Hoge:
...
if __name__ == "__main__":
print(Hoge.mro())
すると、結果として以下が得られる。
[<class '__main__.Hoge'>, <class 'object'>]
これに加えて、それぞれのモジュール(クラス)を取得するには次のようにする。
クラスのパス一覧を取得する関数をget_class_paths
と名づけて、python src/hoge.py
して実際に動作させてみる。
import sys
class Hoge:
@classmethod
def get_class_paths(cls):
for c in cls.mro():
m = sys.modules[c.__module__]
print(m)
if __name__ == "__main__":
print(Hoge.get_class_paths())
結果として以下が得られる。
<module '__main__' from '/path/to/src/hoge.py'>
<module 'builtins' (built-in)>
sys.modules
の説明は公式ドキュメントを参照。端的に言えば、実行中のPythonプロセスのモジュール名とモジュール自体が入った辞書。
これに対して、cls.mro()
で列挙した親クラスのモジュール名をc.__module__
で参照し、そこから実際のモジュールをsys.modules
より取得している。
得られるモジュールとしては、Hoge
クラスが属す__main__
(今回はpython src/hoge.py
を実行したので、hoge.py
が__main__
になる)他に、object
クラスが属すモジュールであるbuiltins
がある。
モジュールからそのファイルパスを参照するには以下のようにする。
import sys
class Hoge:
@classmethod
def get_class_paths(cls):
for c in cls.mro():
m = sys.modules[c.__module__]
path = m.__file__
__file__
を参照するとそのモジュールが定義されているファイルの絶対パスがstr
型で得られる。__file__
については公式ドキュメントを参照。
ここで、Pythonの組み込み関数などが定義されているbuiltins
モジュールには、__file__
が存在しないのでAttributeError
が出る。
これはどうしようもなさそうなので、適当に以下のようにする。
import sys
class Hoge:
@classmethod
def get_class_paths(cls):
for c in cls.mro():
m = sys.modules[c.__module__]
try:
path = m.__file__
except AttributeError: # this module is `builtins` module
continue
print(path)
これで再度python src/hoge.py
を実行すると、結果として以下が得られる。
/path/to/src/hoge.py
次に、Hoge
の継承先でどのように動作するかを確認する。
src/fuga.py
を以下のように書き換える。
from hoge import Hoge
class Fuga(Hoge):
...
if __name__ == "__main__":
print(Fuga.get_class_paths())
python src/fuga.py
を実行すると次の結果が得られる。
/path/to/src/fuga.py
/path/to/src/hoge.py
Hoge
とそれを継承したFuga
のクラス定義が存在するファイルのパスが列挙されており、よさそう。
今回はHoge
が特に何も継承していないので問題はないが、現状の実装では外部ライブラリを利用する際に問題が生じることがある。
具体的には、外部ライブラリのクラスを継承してHoge
を定義する場合、その外部ライブラリのクラスのファイルパスまでこの結果に入ってきてしまう。
外部ライブラリのファイルを確認する必要がない場合、手元で自分が触れるコードのファイルパスのみを列挙できれば便利。
これは、自分のプロジェクトディレクトリをsrc
とするなら、以下のようにpathlib
のrelative_to
メソッドでsrc
ディレクトリ以下に存在するファイルのみを列挙することができる。
import sys
from pathlib import Path
class Hoge:
@classmethod
def get_class_paths_in_my_project(cls):
src_dir = Path.cwd() / "src"
for c in cls.mro():
m = sys.modules[c.__module__]
try:
path = m.__file__
except AttributeError: # this module is `builtins` module
continue
try:
path.relative_to(src_dir)
except ValueError: # not in src dir
continue
print(path)
これは、pathlib.Path
のrelative_to
メソッドがsrc
ディレクトリからの相対パスを求める際に、src
より上のディレクトリにあるものとの相対パスを求めようとすると、例外を吐くという仕様を利用している。
上の機構を加えて、もろもろいい感じにした完成形が以下。
import sys
from pathlib import Path
class Hoge:
@classmethod
def get_class_paths_in_my_project(cls):
# specify the root directory of our project
src_dir = Path.cwd() / "src"
ret = []
# lists classes with Method Resolution Order (MRO)
for c in cls.mro():
m = sys.modules[c.__module__]
try:
path = Path(m.__file__)
except AttributeError: # this module is `builtins` module
continue
try:
path.relative_to(src_dir)
except ValueError: # not in src dir
continue
ret.append(path)
return ret
@classmethod
def print_paths(cls):
print(cls.__name__)
for path in cls.get_class_paths_in_my_project():
print(path)
if __name__ == "__main__":
Hoge.print_paths()
from hoge import Hoge
class Fuga(Hoge):
...
if __name__ == "__main__":
Fuga.print_paths()
from fuga import Fuga
class Piyo(Fuga):
...
if __name__ == "__main__":
Piyo.print_paths()
また、それぞれのファイルをpython src/...
で実行すると、それぞれ以下の結果が得られる。
Hoge
/path/to/src/hoge.py
Fuga
/path/to/src/fuga.py
/path/to/src/hoge.py
Piyo
/path/to/src/piyo.py
/path/to/src/fuga.py
/path/to/src/hoge.py
無事、それぞれの継承先クラスから、欲しいファイルパスが得られていることが確認できた。
あとがき
cls.__name__
で継承先のクラスの名前が取れるのも覚えておいてもいいかもしれない。
かなりニッチな気がするが、何かに役立てば幸いです。
Discussion