このBlogは移転しました。今後は aish.dev を御覧ください。

Zipファイル一個で実行可能なPythonアプリケーションを作ってみる

アプリケーションを作成して配布するとき、配布するのは複数のファイルやディレクトリではなく、ファイル一つだけで済ませることができたらそれに越したことはないだろう。Pythonには cx_Freezepy2exe のような、プラットフォーム固有な実行可能ファイルを開発するための環境も揃っているが、ここではもうちょっとライトに、Pythonの実行環境があれば実行できる zip ファイルの作り方を紹介したい。例として、flaskを使った簡単なWebアプリケーションを作ってみよう。

尚、ここで作成したファイルは https://github.com/atsuoishimoto/demo_pkgrsrc に置いてあるのでご参照いただきたい。

リソースファイルの使い方

普通、アプリケーションにはスクリプトファイル以外にいろんなファイルが含まれる。この例のようなWebアプリケーションではHTMLやCSSが必要だし、Flask では jinja2 のテンプレートファイルも必要となる。 Flask では、こういったファイルをアプリケーションのパッケージディレクトリ内に格納するようになっている。

<Pythonパッケージディレクトリ>
    |- __init__.py
    |- app.py
    |
    |-<template>
    |     |- index.html
    |     |- app.html
    |
    |-<static>
          |- fav.ico
          |- style.css

このような配置にしておけば、Flask が自動的に必要なファイルを読み込んでくれるのだが、アプリケーションを zipファイルに固めてしまうとその面倒を見てくれなくなってしまい、自分でなんとかしなければならない。

このようにパッケージを zipファイルとして作成する場合、データファイルを参照するときには Distribute (または Setuptools) のpkg_resources を使うと、パッケージがzipでもディレクトリでも、どちらでも正しくファイルを読み込むことができるようになる。

例えば、my_application パッケージ内の、static/default.cssというファイルが必要なら、

    pkg_resources.resource_string("my_application", "static/default.css")

でzipファイルでもファイルシステム上のディレクトリでも取得することができる。

flaskでリソースファイルを使う

デフォルトでは、Flask はzipファイルからはスタティックファイルを読み込んでくれないので、ちょっとした細工をする必要がある。普通のFlaskアプリケーションでは

from flask import Flask

app = Flask(__name__)

@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

if __name__ == "__main__":
    app.run()

のようにFlaskオブジェクトを作成するが、ここではzipファイルからの読み込みをサポートするために send_static_file() メソッドを置き換えて pkg_resources のリソースAPIを利用するようにカスタマイズする必要がある。

import pkg_resources
from flask import Flask, helpers

class ZippedFlask(Flask):
    PACKAGENAME = "demo_pkgrsrc"
    
    def send_static_file(self, filename):
        fname = helpers.safe_join(self._static_folder, filename)
        f = pkg_resources.resource_stream(self.PACKAGENAME, fname)
        return helpers.send_file(f, attachment_filename=filename)
            
app = ZippedFlask(__name__)

if __name__ == "__main__":
    app.run()

jinja2でリソースファイルを使う

jinja2を使用する場合は、jinja2のテンプレートファイルもリソースAPIを利用して読み込むようにしなければならない。jinja2にはこの為のjinja2.loaders.PackageLoader() が用意されている。

import pkg_resources
from flask import Flask, helpers

class ZippedFlask(Flask):
    PACKAGENAME = "demo_pkgrsrc"
    
    @helpers.locked_cached_property
    def jinja_loader(self):
        return loaders.PackageLoader(self.PACKAGENAME)

app = ZippedFlask(__name__)

@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

if __name__ == "__main__":
    app.run()

zipファイルを使って実行する

パッケージディレクトリを圧縮してzipファイルを作ってしまえば、環境変数PYTHONPATHへの指定などからsys.pathに登録して、Pythonからインポートできるようになる。

$ zip -r myapp.zip demo_pkgrsrc
$ export PYTHONPATH=myapp.zip
$ python -m demo_pkgrsrc.demo

もうちょっとだけ便利なzipファイルにする

Python2.5以降では、__main__.pyというファイルを含むディレクトリやzipファイルはPythonコマンドにスクリプトファイルとして指定できるようになっている。

以下のような__main__.pyファイルを作成し

import demo_pkgrsrc.demo
demo_pkgrsrc.demo.app.run()

zipファイルのトップディレクトリに含めておけば、 python myapp.zip で実行可能なzipファイルとなる。

$ zip -r myapp.zip __main__.py demo_pkgrsrc
$ python myapp.zip

Windows環境では、このzipファイルの拡張子.py.pyw などの、Pythonに関連付けられた拡張子に変更すれば、エクスプローラからのダブルクリックだけで起動できるようになる。

Unixで実行可能なファイルにする

さらに、Unix環境ではshebangを指定して直接実行可能なファイルとすることもできる。

$ echo '#!/usr/bin/env python' > myapp
$ cat myapp.zip >>myapp
$ chmod +x myapp
$ ./myapp