土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

Python+FlaskのファイルをCython化してみる

前回 は、簡単な Python の Webアプリケーションとして、WSGI を使った、wsgiref、Werkzeug、Flask を動かしてみました。

今回は、Flask で使った Python ファイルを Cython化してみたいと思います。Cython化するメリットは、Pythonの高速化が大きいですが、セキュリティ的に分かりにくいソースコードになるという点もあるようです。

それでは、やっていきます。

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧

開発環境は、VirtualBox + ParrotOS 6.1 です。

Cython化する方法

Cython化する方法は、いくつかあるようです。ウィキペディアにまとまってると助かったのですが、残念ながら内容が薄かったです。仕方ないので、公式サイトに行きます。

cython.org

  • setup.py を作り、cythonizeメソッドの引数に、pyxファイルを渡しておき、setup.py を実行することで、soファイルが生成させる方法
  • pyxファイルに対して、cythonizeコマンドを使ってコンパイルすることで、soファイルが生成させる方法

Cython化してみる

Cython化の対象は、前回 作成した hello.py とします。

Cython化する場合、ファイルの拡張子は pyx にする必要があるようなので、hello.py をコピーして、hello.pyx としました。ファイルの中身は同じです。

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

Cython化する方法は、以下の公式ドキュメントを参考にやっていきます。

cython.readthedocs.io

cythonのインストール

まず、Cython をインストールします。pip で簡単にインストールできました。

$ pip install cython
Successfully installed cython-3.0.10

setup.pyを使った方法でCython化する

参考にする Cython の公式ドキュメントのチュートリアルです。

cython.readthedocs.io

本来は、hello.pyx の中身を Cython化のメリットが出るように、書き換える方がいいのだと思いますが、まずは、中身を変えずにやってみます。

setup.py が必要なので作ります。チュートリアルの通りに作ります。

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize( ['hello.pyx'] )
)

では、早速 setup.py を実行して、ビルドしてみます。

$ python setup.py build_ext --inplace
running build_ext
building 'hello' extension
creating build
creating build/temp.linux-x86_64-cpython-311
x86_64-linux-gnu-gcc -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -fPIC -I/home/user/20240731_flask/include -I/usr/include/python3.11 -c hello.c -o build/temp.linux-x86_64-cpython-311/hello.o
creating build/lib.linux-x86_64-cpython-311
x86_64-linux-gnu-gcc -shared -Wl,-O1 -Wl,-Bsymbolic-functions -g -fwrapv -O2 build/temp.linux-x86_64-cpython-311/hello.o -L/usr/lib/x86_64-linux-gnu -o build/lib.linux-x86_64-cpython-311/hello.cpython-311-x86_64-linux-gnu.so
copying build/lib.linux-x86_64-cpython-311/hello.cpython-311-x86_64-linux-gnu.so ->

どういうファイルが作られたか、見てみます。

hello.pyx と同じ階層に、hello.c というCソースコードと、soファイルが作られました。それとは別に、buildディレクトリが作られて、その中に、オブジェクトファイル(hello.o)と、hello.pyx と同じ階層の soファイルと同じファイル(diffコマンドで確認済み)が置かれています。

$ tree
.
|-- __pycache__
|   `-- hello.cpython-311.pyc
|-- build
|   |-- lib.linux-x86_64-cpython-311
|   |   `-- hello.cpython-311-x86_64-linux-gnu.so
|   `-- temp.linux-x86_64-cpython-311
|       `-- hello.o
|-- hello.c
|-- hello.cpython-311-x86_64-linux-gnu.so
|-- hello.py
|-- hello.pyx
`-- setup.py

5 directories, 8 files

では、実行してみます。

同じディレクトリに hello.py があると、どちらが実行されたか分からなくなるので、exeディレクトリを新しく作り、そこで実行してみて、実行できないことを確認します。その後、そこに soファイルのシンボリックファイルを作って、そこで実行してみます。

$ mkdir exe
$ cd exe/
$ flask --app hello run
Usage: flask run [OPTIONS]
Try 'flask run --help' for help.

Error: Could not import 'hello'.

$ ln -s ../hello.cpython-311-x86_64-linux-gnu.so
$ tree ../
../
|-- __pycache__
|   `-- hello.cpython-311.pyc
|-- build
|   |-- lib.linux-x86_64-cpython-311
|   |   `-- hello.cpython-311-x86_64-linux-gnu.so
|   `-- temp.linux-x86_64-cpython-311
|       `-- hello.o
|-- exe
|   `-- hello.cpython-311-x86_64-linux-gnu.so -> ../hello.cpython-311-x86_64-linux-gnu.so
|-- hello.c
|-- hello.cpython-311-x86_64-linux-gnu.so
|-- hello.py
|-- hello.pyx
`-- setup.py

6 directories, 9 files

$ flask --app hello run
 * Serving Flask app 'hello'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

$ sudo lsof -i | grep user
flask     2359   user    3u  IPv4  51036      0t0  TCP localhost:5000 (LISTEN)

意図通り、soファイルがカレントディレクトリにあるだけで、Flask の起動が出来ました。

参考までに、作成された hello.c を見ておきます。6391行(239KB)もあるので、hello.py の部分と思われるところだけ貼ります。さすがに、人間が読むのは、しんどそうです。

/* #### Code section: module_code ### */

/* "hello.pyx":5
 * app = Flask(__name__)
 * 
 * @app.route("/")             # <<<<<<<<<<<<<<
 * def hello_world():
 *     return "<p>Hello, World!</p>"
 */

/* Python wrapper */
static PyObject *__pyx_pw_5hello_1hello_world(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused); /*proto*/
static PyMethodDef __pyx_mdef_5hello_1hello_world = {"hello_world", (PyCFunction)__pyx_pw_5hello_1hello_world, METH_NOARGS, 0};
static PyObject *__pyx_pw_5hello_1hello_world(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused) {
  CYTHON_UNUSED PyObject *const *__pyx_kwvalues;
  PyObject *__pyx_r = 0;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("hello_world (wrapper)", 0);
  __pyx_kwvalues = __Pyx_KwValues_VARARGS(__pyx_args, __pyx_nargs);
  __pyx_r = __pyx_pf_5hello_hello_world(__pyx_self);

  /* function exit code */
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

static PyObject *__pyx_pf_5hello_hello_world(CYTHON_UNUSED PyObject *__pyx_self) {
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("hello_world", 1);

  /* "hello.pyx":7
 * @app.route("/")
 * def hello_world():
 *     return "<p>Hello, World!</p>"             # <<<<<<<<<<<<<<
 */
  __Pyx_XDECREF(__pyx_r);
  __Pyx_INCREF(__pyx_kp_s_p_Hello_World_p);
  __pyx_r = __pyx_kp_s_p_Hello_World_p;
  goto __pyx_L0;

  /* "hello.pyx":5
 * app = Flask(__name__)
 * 
 * @app.route("/")             # <<<<<<<<<<<<<<
 * def hello_world():
 *     return "<p>Hello, World!</p>"
 */

  /* function exit code */
  __pyx_L0:;
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

cythonizeコマンドを使う方法でCython化する

先ほどと同じディレクトリで実行すると、ファイルが混ざってしまいそうなので、別のディレクトリを作り、hello.pyx だけをコピーしておきます。

では、早速 cythonizeコマンドを実行してみます。-a オプションはソースコードの注釈付きの HTMLファイルが生成されます。-i オプションは --inplace と同じで、このディレクトリに soファイルが生成されます。

$ cythonize -a -i hello.pyx
Compiling /home/user/svn/flask/tutorial2/hello.pyx because it changed.
[1/1] Cythonizing /home/user/svn/flask/tutorial2/hello.pyx
/home/user/20240731_flask/lib/python3.11/site-packages/Cython/Compiler/Main.py:381: FutureWarning: Cython directive 'language_level' not set, using '3str' for now (Py3). This has changed from earlier releases! File: /home/user/svn/flask/tutorial2/hello.pyx
  tree = Parsing.p_module(s, pxd, full_module_name)

成功しました。出力されたメッセージは、先ほどの setup.py を使った方法とは、全く異なりますね。setup.py を使った方法の方が、コンパイルしてる感じでした。

では、生成されたファイルを確認してみます。

$ tree
.
|-- build
|   `-- lib.linux-x86_64-cpython-311
|       `-- hello.cpython-311-x86_64-linux-gnu.so
|-- hello.c
|-- hello.cpython-311-x86_64-linux-gnu.so
|-- hello.html
`-- hello.pyx

3 directories, 5 files

先ほどとの違いは、オブジェクトファイルが出力されていないことと、HTMLファイルが生成されていることでしょうか。先ほどと同様に、soファイルに差異はありませんでした。

hello.c を比べてみました。ファイルパスの文字列だけが異なっているだけで、それ以外は同じソースコードが生成されたようです。

--- hello.c     2024-07-31 22:55:56.083132149 +0900
+++ ../tutorial/hello.c 2024-07-31 22:11:51.724087415 +0900
@@ -5,7 +5,7 @@
     "distutils": {
         "name": "hello",
         "sources": [
-            "/home/user/svn/flask/tutorial2/hello.pyx"
+            "hello.pyx"
         ]
     },
     "module_name": "hello"

では、実行してみます。

先ほどと同様に、空のディレクトリを作って、その中で実行します。

$ mkdir exe
$ cd exe/
$ flask --app hello run
Usage: flask run [OPTIONS]
Try 'flask run --help' for help.

Error: Could not import 'hello'.

$ ln -s ../hello.cpython-311-x86_64-linux-gnu.so
$ tree ../
../
|-- build
|   `-- lib.linux-x86_64-cpython-311
|       `-- hello.cpython-311-x86_64-linux-gnu.so
|-- exe
|   `-- hello.cpython-311-x86_64-linux-gnu.so -> ../hello.cpython-311-x86_64-linux-gnu.so
|-- hello.c
|-- hello.cpython-311-x86_64-linux-gnu.so
|-- hello.html
`-- hello.pyx

4 directories, 6 files

$ flask --app hello run
 * Serving Flask app 'hello'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

$ sudo lsof -i | grep user
flask     79983   user    3u  IPv4 241061      0t0  TCP localhost:5000 (LISTEN)

先ほどと同じように実行できているようです。

おわりに

今回は、少し寄り道をして、PythonスクリプトをCython化してみました。

Cython のロゴは、Python のロゴを「C」で囲ったものでした。興味深いです(笑)。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。