読者です 読者をやめる 読者になる 読者になる

モジュールの遅延ロードについて

AppEngineでスピンアップが遅くなり過ぎないようにとか考えると、Pythonでもモジュールの遅延ロードをしないといけないわけで、まあ書く。

app.yaml

この例ではすべてのリクエストをmain.pyで受ける。

application: nullpobug-sandbox
version: 1
runtime: python
api_version: 1

handlers:
- url: .*
  script: main.py

main.py

WSGIアプリケーションの形にしてます。PATH_INFOの値で実行するアプリケーションを切り替えてます。

# coding: utf-8
import sys

from google.appengine.ext.webapp import util

def load_app(name):
    """
    モジュールからアプリケーション関数をロードする関数
    """
    bits = name.split('.')
    module_name = '.'.join(bits[:-1])
    app_name = bits[-1]
    # モジュールをインポート(ロード)
    __import__(module_name, {}, {}, [])
    module = sys.modules[module_name]
    # モジュールから属性名で参照して返す
    return getattr(module, app_name)

# キャッシュ用変数
apps = {}

def get_app(name):
    """
    キャッシュからアプリケーション関数を返す
    キャッシュになければモジュールからロードする
    """
    global apps
    # ロード済みか判定
    if name in apps:
        app = apps[name]
    else:
        app = load_app(name)
        # キャッシュする
        apps[name] = app
    return app

def entrypoint(environ, start_response):
    path = environ['PATH_INFO']
    # URLで実行するアプリケーションを切り替える
    if path == '/':
        app = get_app('app1.application')
    elif path == '/app2/':
        app = get_app('app2.application')
    else:
        # マッチしないので404
        start_response('404 NotFound', [('Content-Type', 'text/plain')])
        return '404 NotFound'
    return app(environ, start_response)

def main():
    util.run_wsgi_app(entrypoint)

if __name__ == '__main__':
    main()

app1.py

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return 'app1.application'

app2.py

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return 'app2.application'

これで、 "/" にアクセスすると "app1.application" と表示され、 "/app2/" でアクセスすると "app2.application" と表示される。マッチしない場合は404になる。
まあwerkzeugとかdjangoなどのフレームワーク、モジュールだと同様のコードを持ってたりしますがね。
warmup使えばそんなの気にしなくていいぜ!とかあるかもしれませんが、コード量が多くてロードが遅すぎると結局タイムアウトしてしまったりするので、その対策としても使える。

追記

そういえばこの例だとロードしたアプリケーションがモジュールの関数なのでキャッシュする意味があんまりないことに気づいた。
クラスをロードしてインスタンスをキャッシュするとかだと、意味はある。

さらに追記

コメントもらったので。この例では文字列からインポートする方法を説明しようと思ったんだけど(フレームワークを作るときなんかに使う)、その説明が抜けていた。
もちろん、単に関数の中でインポートしても問題ないです。

if path == '/':
    from app1 import application
    return application(environ, start_response)

djangoのコードにもこういうのがいっぱいあります。