Python
python3

PEP 562 -- Module __getattr__ and __dir__を試してみた

Python3.7の新機能、__getattr__と__dir__関数を試してみた。

PEP 562 -- Module __getattr__ and __dir__ | Python.org

典型的な例としてRationaleに2つサンプルが記載されている。

  • 廃止予定の警告の管理
  • サブモジュールの遅延インポート

2つのうち、サブモジュールの遅延インポートについてサンプルを考えてみた。

データを読み込む際に、読み込み先がファイルとデータベースで切り替えられる仕組みについて実装した。
main.pyのloader_settingで、LoaderDefのどちらを代入するかで、読み込み先が変わる。
→ main.pyに書いてしまっているけど、これを他の設定と一緒に設定ファイルでまとめて定義するといいと思う

getattr/__init__.py
import importlib


__all__ = ['file_loader', 'db_loader']


def __getattr__(name):
    if name in __all__:
        return importlib.import_module("." + name, __name__)
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
getattr/loader.py
from abc import ABCMeta, abstractmethod


print('loader module')


class Loader(metaclass=ABCMeta):

    print('Loader class')

    @abstractmethod
    def load(self):
        pass
getattr/file_loader.py
from getattr.loader import Loader


print('file_loader module')


class FileLoader(Loader):

    print('FileLoader class')

    def load(self):
        print('FileLoader.load')
getattr/db_loader.py
from getattr.loader import Loader


print('db_loader module')


class DBLoader(Loader):

    print('DBLoader class')

    def load(self):
        print('DBLoader.load')
getattr/main.py
import enum
import getattr


class LoaderDef(enum.Enum):
    FILE_LOADER = 'getattr.file_loader.FileLoader'
    DB_LOADER = 'getattr.db_loader.DBLoader'


loader_setting = LoaderDef.FILE_LOADER
# loader_setting = LoaderDef.DB_LOADER

loader = eval(loader_setting.value)()

loader.load()
実行結果
loader module
Loader class
file_loader module
FileLoader class
FileLoader.load

実行結果を見ると、db_loaderモジュールとDBLoaderクラスが呼ばれていないのがわかる。
なので、使わないオブジェクトの生成を防ぐことができている。

getattr/main2.py
import enum
from getattr.file_loader import FileLoader
from getattr.db_loader import DBLoader


class LoaderDef(enum.Enum):
    FILE_LOADER = 'FileLoader'
    DB_LOADER = 'DBLoader'


loader_setting = LoaderDef.FILE_LOADER
# loader_setting = LoaderDef.DB_LOADER

loader = eval(loader_setting.value)()

loader.load()
実行結果
loader module
Loader class
file_loader module
FileLoader class
db_loader module
DBLoader class
FileLoader.load

サブモジュールの遅延インポートを使わない場合は、db_loaderモジュールとDBLoaderクラスも呼ばれてしまう。

evalを使わずにもっとスマートに書く方法があるか考え中。
(main2.pyの場合、Enumのvalueは文字列じゃなくクラスでいいが、対比を示すために同様にevalを使っている)

補足
main.pyの最後に、

print(dir(getattr))

を追加して実行すると、

['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__getattr__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'file_loader', 'importlib', 'loader']

となり、file_loaderだけがgetattrのアトリビュートとなっていることがわかる。

以下に続く。
PEP 562 -- Module __getattr__ and __dir__を試してみた Part2 - Qiita