LoginSignup
3
5

More than 3 years have passed since last update.

Cythonを使用して、複数ファイルにまたがったPythonコードから実行ファイルを生成(Mac:成功、Linux:失敗、Windows:成功)

Last updated at Posted at 2020-01-20

記事の内容

Pythonのプロジェクトで、ソースコードを納品しないケースに遭遇しました。
Pyinstallerを使用してバイナリ化する手順を別記事に書きましたが、Cythonも試しましたので手順を記録しておきます。
MacとLinuxで試して、Macは成功、Linuxは失敗という結果になりました。
いずれWindowsでも試す予定です。

追記(2020-01-24)記事の最後にWindows編(成功)を追加しました。

サンプルコード

Pythonコードは以下の3ファイルを使用し、foo.pyをバイナリにしたものを起動することにします。

foo.py

from mymod1 import bar
from mymod2 import hoge
import sys

bar("Hello!")
hoge("Hi!")

print("-----------------")
print(sys.path)
print(f"__name__ = {__name__}")
print(f"__file__ = {__file__}")
mymod1.py

def bar(s):
    print(f"bar: {s}")
mymod2.py

import pandas as pd

def hoge(s):
    print(f"hoge: {s}")
    df = pd.DataFrame(index=[])
    print(df)

Mac編(成功の記録)

Python 3.7.4を使用しました。

【Mac編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.so)を生成する

以下のsetup.pyを準備します。

setup.py

from setuptools import setup, Extension
from Cython.Build import cythonize

setup(
    ext_modules=cythonize([
        Extension(
            "mymod1",
            sources=["mymod1.py"],
        ),
        Extension(
            "mymod2",
            sources=["mymod2.py"],
        ),
    ]),
)

端末で以下を実行して、モジュールをインストールします。pipenvを使用しています。


pipenv install setuptools cython pandas

現段階で存在するファイルは以下の通りです。


$ ls
Pipfile
foo.py
mymod1.py
mymod2.py
setup.py

生成されたPipfileは以下の通り。


[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
setuptools = "*"
cython = "*"
pandas = "*"

[requires]
python_version = "3.7"

端末で以下を実行して、.soファイルをビルドします。


pipenv run python setup.py build_ext --inplace

現段階で存在するファイルは以下の通りです。マイモジュールの.cと.soが生成されています。


$ ls
Pipfile
build
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py

生成された共有ライブラリのファイル名がlibで始まっていません。
どうやらこれらの.soファイルは、実行ファイルのビルド時に人がリンクの指定をするものではなく、実行時にdlopen関数で動的にロードされて使われるようです。
最初はそのことに気づきませんでしたが、C言語でdlopen関数を使った遠い記憶があったので、ようやく気づいた(^^;

共有ライブラリ(.so)の動作確認

foo.pyの実行ファイルを生成する前に、先ほど生成された.soを動作確認してみます。
端末で以下を実行します。


mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

pipenv run python foo.py

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のように表示され、正常に動くことが確認できました。

bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/Users/username/PycharmProjects/foo_project', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python37.zip', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/lib-dynload', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/site-packages']
__name__ = __main__
__file__ = foo.py

【Mac編】ステップ2: foo.pyから実行ファイルを生成する

端末で以下を実行して、C言語のソースコードを生成します。
main関数にするために、--embedオプションを使用しています。


pipenv run cython foo.py --embed

現段階で存在するファイルは以下の通りです。foo.cが生成されています。


$ ls
Pipfile
build
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py

foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、探しておきます。

Python.hの場所:


$ find $HOME -type f -name 'Python.h' 2> /dev/null
/Users/username/.pyenv/versions/3.7.4/include/python3.7m/Python.h

ライブラリの場所とライブラリファイル名:


$ cd /Users/username/.pyenv/versions/3.7.4
$ ls
Python.framework
bin
bin.orig
include
lib
share
$ cd lib/
$ ls
lib
libpython3.7m.a
libpython3.7m.dylib
pkgconfig
python3.7
$ pwd
/Users/username/.pyenv/versions/3.7.4/lib

これらの情報を与えて、以下のようにコンパイルします。


gcc foo.c -o foo -I$HOME/.pyenv/versions/3.7.4/include/python3.7m -L$HOME/.pyenv/versions/3.7.4/lib -lpython3.7m

現段階で存在するファイルは以下の通りです。実行ファイルfooが生成されています。


$ ls
Pipfile
build
foo
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py

実行ファイルの動作確認

生成された実行ファイルを動かします。


mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

./foo

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のようにエラーメッセージが表示されました。

Traceback (most recent call last):
  File "foo.py", line 2, in init foo
    from mymod2 import hoge
  File "mymod2.py", line 1, in init mymod2
    import pandas as pd
ModuleNotFoundError: No module named 'pandas'

PYTHONPATHを設定して、やり直します。
pipenvを使用している場合の実行例:

mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

PYTHONPATH=`pipenv --venv`/lib/python3.7/site-packages ./foo

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のように、foo.pyの__name__を表示する行まで成功しました。
__file__を表示する行で落ちていますから、Pythonコードと同じ動きをするとは限らないようです。
ともかく、Macでは成功しました。

bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/Users/username/PycharmProjects/foo_project', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/site-packages', '/Users/username/.pyenv/versions/3.7.4/lib/python37.zip', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7/lib-dynload', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7/site-packages']
__name__ = __main__
Traceback (most recent call last):
  File "foo.py", line 11, in init foo
    print(f"__file__ = {__file__}")
NameError: name '__file__' is not defined

Linux編(失敗の記録)

Ubuntu 18.04、Python 3.7.5を使用しました。

【Linux編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.so)を生成する

ステップ1はMac編と全く同じ手順で成功します。
以下、動作確認した部分のみ貼り付けておきます。

$ pipenv run python foo.py
/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/site-packages/pandas/compat/__init__.py:85: UserWarning: Could not import the lzma module. Your installed Python is incomplete. Attempting to use lzma compression will result in a RuntimeError.
  warnings.warn(msg)
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/var/www/foo_project', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python37.zip', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/lib-dynload', '/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/lib/python3.7', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/site-packages']
__name__ = __main__
__file__ = foo.py

【Linux編】ステップ2: foo.pyから実行ファイルを生成する

Mac編と同様に、端末で以下を実行して、C言語のソースコードを生成します。


pipenv run cython foo.py --embed

現段階で存在するファイルは以下の通りです。foo.cが生成されています。


$ ls
Pipfile
Pipfile.lock
build
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-x86_64-linux-gnu.so
mymod1.py
mymod2.c
mymod2.cpython-37m-x86_64-linux-gnu.so
mymod2.py
setup.py

foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、探しておきます。

Python.hの場所:


$ find $HOME -type f -name 'Python.h' 2> /dev/null
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m/Python.h

ライブラリの場所とライブラリファイル名:


$ cd /home/laradock/.anyenv/envs/pyenv/versions/3.7.5
$ ls
bin
include
lib
share
$ cd lib/
$ ls
libpython3.7m.a
pkgconfig
python3.7
$ pwd
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/lib

これらの情報を与えて、以下のようにコンパイルします。
リンクエラーが出たら、適宜必要なライブラリを加えます。


gcc foo.c -o foo -I$HOME/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m -L$HOME/.anyenv/envs/pyenv/versions/3.7.5/lib -lpython3.7m -lm -lpthread -ldl -lutil

現段階で存在するファイルは以下の通りです。実行ファイルfooが生成されています。


$ ls
Pipfile
Pipfile.lock
build
foo
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-x86_64-linux-gnu.so
mymod1.py
mymod2.c
mymod2.cpython-37m-x86_64-linux-gnu.so
mymod2.py
setup.py

実行ファイルの動作確認

生成された実行ファイルを動かします。


mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py

PYTHONPATH=`pipenv --venv`/lib/python3.7/site-packages ./foo

mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py

以下のようにエラーメッセージが表示されました。

$ ./foo
Traceback (most recent call last):
  File "foo.py", line 1, in init foo
    from mymod1 import bar
ImportError: /var/www/foo_project/mymod1.cpython-37m-x86_64-linux-gnu.so: undefined symbol: PyExc_SystemError

このエラーを回避する方法はまだ発見できておりません(誰か教えて)。

追記(2020-01-24)Windows編(成功の記録)

Windows10でも試して成功しましたので、手順を記録しておきます。
Pythonは3.7.6を、CコンパイラはVisual Studio 2019 Communityを使用しました。

【Windows編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.pyd)を生成する

Mac編と同内容のsetup.pyを準備します。以下に再掲します。

setup.py

from setuptools import setup, Extension
from Cython.Build import cythonize

setup(
    ext_modules=cythonize([
        Extension(
            "mymod1",
            sources=["mymod1.py"],
        ),
        Extension(
            "mymod2",
            sources=["mymod2.py"],
        ),
    ]),
)

「x64 Native Tools Command Prompt for VS 2019」で以下を実行して、モジュールをインストールします。pipenvを使用しています。


pipenv install setuptools cython pandas

現段階で存在するファイルは以下の通りです。


> dir
foo.py
mymod1.py
mymod2.py
Pipfile
setup.py

「x64 Native Tools Command Prompt for VS 2019」で以下を実行して、.pydファイルをビルドします。


pipenv run python setup.py build_ext --inplace

現段階で存在するファイルは以下の通りです。マイモジュールの.cと.pydが生成されています。


> dir
build
foo.py
mymod1.c
mymod1.cp37-win_amd64.pyd
mymod1.py
mymod2.c
mymod2.cp37-win_amd64.pyd
mymod2.py
Pipfile
setup.py

共有ライブラリ(.pyd)の動作確認

foo.pyの実行ファイルを生成する前に、先ほど生成された.pydを動作確認してみます。
「x64 Native Tools Command Prompt for VS 2019」で以下を実行します。


move mymod1.py _mymod1.py
move mymod2.py _mymod2.py

pipenv run python foo.py

move _mymod1.py mymod1.py
move _mymod2.py mymod2.py

以下のように表示され、正常に動くことが確認できました。


bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['C:\\Users\\username\\PycharmProjects\\foo_project', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Lib\\site-packages', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Scripts\\python37.zip', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\DLLs', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\lib', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Scripts', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\Lib', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\DLLs', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS']
__name__ = __main__
__file__ = foo.py

【Windows編】ステップ2: foo.pyから実行ファイルを生成する

「x64 Native Tools Command Prompt for VS 2019」で以下を実行して、C言語のソースコードを生成します。
main関数にするために、--embedオプションを使用しています。


pipenv run cython foo.py --embed

現段階で存在するファイルは以下の通りです。foo.cが生成されています。


>dir
build
foo.c
foo.py
mymod1.c
mymod1.cp37-win_amd64.pyd
mymod1.py
mymod2.c
mymod2.cp37-win_amd64.pyd
mymod2.py
Pipfile
setup.py

foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、何らかの方法で探しておきます。
私の環境では、以下の場所にありました。

Python.hの場所:


>dir C:\Users\username\AppData\Local\Programs\Python\Python37\include\Python.h

 C:\Users\username\AppData\Local\Programs\Python\Python37\include のディレクトリ

2019/12/18  23:41             3,713 Python.h

ライブラリの場所:


>dir C:\Users\username\AppData\Local\Programs\Python\Python37\libs

 C:\Users\username\AppData\Local\Programs\Python\Python37\libs のディレクトリ

2019/12/19  00:45         1,232,690 libpython37.a
2019/12/19  00:43           170,564 python3.lib
2019/12/19  00:42           342,420 python37.lib
2019/12/19  00:43             1,750 _tkinter.lib

これらの情報を与えて、以下のようにコンパイルします。


cl foo.c /IC:\Users\username\AppData\Local\Programs\Python\Python37\include /link C:\Users\username\AppData\Local\Programs\Python\Python37\libs\python37.lib

現段階で存在するファイルは以下の通りです。実行ファイルfoo.exe等が生成されています。


>dir
build
foo.c
foo.exe
foo.exp
foo.lib
foo.obj
foo.py
mymod1.c
mymod1.cp37-win_amd64.pyd
mymod1.py
mymod2.c
mymod2.cp37-win_amd64.pyd
mymod2.py
Pipfile
setup.py

実行ファイルの動作確認

生成された実行ファイルを動かします。


move mymod1.py _mymod1.py
move mymod2.py _mymod2.py

.\foo.exe

move _mymod1.py mymod1.py
move _mymod2.py mymod2.py

以下のようにエラーメッセージが表示されました。


Traceback (most recent call last):
  File "foo.py", line 2, in init foo
    from mymod2 import hoge
  File "mymod2.py", line 1, in init mymod2
ModuleNotFoundError: No module named 'pandas'

PYTHONPATHを設定して、やり直します。
pipenvを使用している場合の実行例:
(PYTHONPATHの設定を一時的にしたいので、コマンドプロンプト上で更にコマンドプロンプトを起動し、処理終了後に抜けています)


>cmd
Microsoft Windows [Version 10.0.18362.592]
(c) 2019 Microsoft Corporation. All rights reserved.

>pipenv --venv
C:\Users\username\.virtualenvs\foo_project-HtF6OWVS

>set PYTHONPATH=C:\Users\username\.virtualenvs\foo_project-HtF6OWVS\Lib\site-packages

>move mymod1.py _mymod1.py
        1 個のファイルを移動しました。

>move mymod2.py _mymod2.py
        1 個のファイルを移動しました。

>.\foo.exe
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['C:\\Users\\username\\PycharmProjects\\foo_project', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Lib\\site-packages', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\python37.zip', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\Lib', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\DLLs', 'C:\\Users\\username\\PycharmProjects\\foo_project', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\lib\\site-packages']
__name__ = __main__
Traceback (most recent call last):
  File "foo.py", line 11, in init foo
    print(f"__file__ = {__file__}")
NameError: name '__file__' is not defined

>move _mymod1.py mymod1.py
        1 個のファイルを移動しました。

>move _mymod2.py mymod2.py
        1 個のファイルを移動しました。

>exit

Mac編と同様に、foo.pyの__name__を表示する行まで成功しました。
__file__を表示する行で落ちているのも、Mac編と同じです。

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5