Python for .NET for Python for .NET for …

いずれはやりたくなる。

例によって「入り口だけは見ておこう」と。タイトルの意味、わかるよね? Python と .NET を行ったり来たりしたい、という話。無論こういうことをし出せば「自分が今どっちにいるのかすぐに迷子になる」病にかかるので注意。

「あなたさまにおかれましては思ったよりは複雑な例」的なものでカッコつけようかと思ったが、うまくいかなかったので、「うまくいったこと」だけ見せることでカッコつけることにした。無論そんなことは秘密である。

今回は 前回 までのノリのように「一個のスクリプトで C# コーディング、ビルド、実行」なんてことをやると却ってウザいことになるので、全部別々。それと前回こだわった「Python.Runtime.dllpythonXX.dll をコピーしたかぁない」は、「教育のためにも」そうはせずに素直にコピーする。

こういう階層:

MSYS bash より。
1 [me@host: pynexam]$ find .
2 .
3 ./build_cs.py
4 ./lib
5 ./lib/mymod.py
6 ./PyFromCSExamLib.cs
7 ./pyfromcsexamlib_driver.py

やりたいことは、「Python for .NET を使ったドライバ(Python) が、Python for .NET を使った C# を呼び出し、呼び出された C# が別の自作 python モジュールを呼び出す」である。ということは実は「やりたかったええカッコしい」をやらないなら 前回 とネタとしてはそんなに大差なかったりするんだけど、このまま意識高い系を続ける。

ちぅわけで、まずはビルドスクリプト:

 1 # -*- coding: utf-8 -*-
 2 # ---------------------------------------------------
 3 from __future__ import absolute_import
 4 from __future__ import unicode_literals
 5 from __future__ import print_function
 6 
 7 
 8 if __name__ == '__main__':
 9     # 今回の例は C# コードはちゃんと独立させる。「py ⇒ cs ⇒ py」という
10     # 行き来をやりたいので一個に書き切っちゃうとかえってウザいので。
11     # のでここではビルドだけ。
12     import subprocess, os, sys
13 
14     # pythonXX.dll と site-packages にある Python.Runtime.dll
15     #   今回は「コピーするのはやだ」とせず、Python.Runtime.dll、
16     #   pythonXX.dll ともにカレントにコピーする。
17     import site, shutil
18     from distutils import sysconfig
19     cands = site.getsitepackages()
20     for canddir in cands[1:]:
21         pythonengine_dll = os.path.join(canddir, "Python.Runtime.dll")
22         if os.path.exists(pythonengine_dll):
23             break
24     pydllpath = "{}/python{}.dll".format(
25         sysconfig.get_config_var("BINDIR"),  # c:/Python27/
26         sysconfig.get_config_var("VERSION")  # 27
27         )  # 一応: 共有ライブラリ名のお約束は win と unix で違うので注意。
28     #      #       (unix 系は「libpython2.7.so」などとなる。)
29     shutil.copyfile(pythonengine_dll, os.path.basename(pythonengine_dll))
30     shutil.copyfile(pydllpath, os.path.basename(pydllpath))
31 
32     #    ビルドしちゃいなよ
33     subprocess.check_call(
34         [
35             # csc がない、と言われる場合で「ほんとうは持ってる」場合は、
36             # BuildTools 2007 なら、スタートメニューから「x64_x86 Cross Tools
37             # Command Prompt for VS 2017」(等)を探して起動し、その環境から。
38             "csc.exe",
39             "-nologo",
40             "-target:library",  # as dll
41             "-out:PyFromCSExamLib.dll",
42             "-reference:" + os.path.basename(pythonengine_dll),
43             #"-reference:" + pythonengine_dll,
44             "PyFromCSExamLib.cs",
45             ]
46         )

Python.Runtime.dllpythonXX.dll を素直にコピーして、C# を単にビルドしてるだけだが、今回の C# は EXE ではなく DLL なので、「-target:」が前回と違う。

lib/mymod.py は今回は単純のために「Python for .NET に依存しない普通の python」にしてみる。:

1 # -*- coding: utf-8 -*-
2 def somefun():
3     print("I'm mymod.py, and hello anybody!")

くそおもしろくもない。

で次が C#。Python for .NET を使って書いているわけである:

PyFromCSExamLib.cs
 1 using System;  // for Console, etc.
 2 //using System.IO;  // for Path
 3 //using System.Reflection;  // for Assembly
 4 
 5 using Python.Runtime;
 6 
 7 namespace PyFromCSExamLib
 8 {
 9     public class ExamLib
10     {
11         static ExamLib()
12         {
13             // python モジュール検索パスを知らしめてあげる、としたかったが…
14             // 制御方法わからず。ゆえ、本当に環境変数を外から設定しなはれ。
15             // MSYS bash からの例:
16             //   (PYTHONPATH="`pwd -W`/lib" ; py -3 ./pyfromcsexamlib_driver.py)
17         }
18         public static void ExecMyPy()
19         {
20             using (Py.GIL())
21             {
22                 // mymod.py は lib/ にいる「Python for .NET」非依存のモジュール、として。
23                 dynamic mymod = Py.Import("mymod");
24                 mymod.somefun();
25             }
26         }
27         public static int ExecFromMyPy(int i)
28         {
29             // ExecFromMyPy を呼び出すのは「Python for .NET」依存のモジュールだとして。
30             return i * 10;
31         }
32     }
33 }

あとはこの C# を呼び出す Python で書いたドライバ、無論 Python for .NET を使う:

pyfromcsexamlib_driver.py
 1 # -*- coding: utf-8 -*-
 2 # ---------------------------------------------------
 3 from __future__ import absolute_import
 4 from __future__ import unicode_literals
 5 from __future__ import print_function
 6 
 7 
 8 if __name__ == '__main__':
 9     import clr
10     from System.Reflection import Assembly
11     clr.AddReference("PyFromCSExamLib")
12 
13     import PyFromCSExamLib
14     PyFromCSExamLib.ExamLib.ExecMyPy()  # me -> c# -> lib/mymod.py
15     print(
16         PyFromCSExamLib.ExamLib.ExecFromMyPy(10)
17         # me -> c# -> return to me
18         )

実際ビルドスクリプトを動かしてビルドして、pyfromcsexamlib_driver.py を動かすと「正しく動作しない」、が、これは当然ワタシは「やる前から知っている」。あえてそういう例を選んだのだから。

もちろんこれは Python モジュール検索パスの問題で、例えば環境変数 PYTHONPATH である:

MSYS bash より。
1 [me@host: pynexam]$ (PYTHONPATH="`pwd -W`/lib" ; py -3 ./pyfromcsexamlib_driver.py)
2 I'm mymod.py, and hello anybody!
3 100

というわけで個人ユースで済む分にはこれで存分に十分だ。(多分 pyfromcsexamlib_driver.py が面倒みてしまう手はある。)

この PYTHONPATH 制御を C# 側で出来んかなぁ、と画策したのさ。プロジェクトのポリシーなんぞにも大いに依存するだろうけれど、C# 側が制御を握っていた方がやりやすいこともあるだろうからね。けどどうにも制御の仕方がわからない。.NET の Environment.SetEnvironmentVariable を呼び出すのはこれは「手遅れ」でダメ。PythonEngine.PythonPath を上書くのも試みてはみたがこれもダメ、おそらくこれも結局「手遅れ」てことかな。CPython の C API だと素直にコントロール出来たんだけどなぁ…。

ただこれ、解決出来ないことはないだろうなと思ってるんで、わかったらここに追記する。

あと、「入り口を見てみたいだけなのね」な都合、色々「ほんとは知りたい色んなこと」を器用に避けてる。そう、「Python / C# 間の型変換」の件ね。これは一応公式サイトに説明があるし、「やり出すとそれだけでたくさんの繊細なケースを検証してみねばならぬ」と思うので、「出来るとわかっている」パターンだけ使った。ご注意。


21:50追記:
若干の事前条件は付くものの、「あくまでも python と同じ流儀で」:

PyFromCSExamLib.cs
 1 using System;  // for Console, etc.
 2 //using System.IO;  // for Path
 3 //using System.Reflection;  // for Assembly
 4 
 5 using Python.Runtime;
 6 
 7 namespace PyFromCSExamLib
 8 {
 9     public class ExamLib
10     {
11         static ExamLib()
12         {
13             using (Py.GIL())
14             {
15                 dynamic sys = Py.Import("sys");
16                 sys.path.insert(0, "lib");
17             }
18         }
19         public static void ExecMyPy()
20         {
21             using (Py.GIL())
22             {
23                 // mymod.py は lib/ にいる「Python for .NET」非依存のモジュール、として。
24                 dynamic mymod = Py.Import("mymod");
25                 mymod.somefun();
26             }
27         }
28         public static int ExecFromMyPy(int i)
29         {
30             // ExecFromMyPy を呼び出すのは「Python for .NET」依存のモジュールだとして。
31             return i * 10;
32         }
33     }
34 }

これで「オレだけに的ニーズ」ならオーケー。(よくよく思い出してみると、ワタシが C/C++ への埋め込み Python をやったときはまさにこれをしてた。)

さて。注意深い人はわかることなんだけれど、「python35.dll をコピー」に反応すべきである。これが仮に「ヒトさまのためのもの」を作っていて、そのために「インストーラを作ろう」と思ってみればすぐさまわかる。

すなわち、「python35.dll を同梱」して配布するつもりならば、「標準ライブラリ」をどうするか決断しなければならない。「オレんちなら dynamic sys = Py.Import("sys"); が動く」のは「オレんちの python が動く環境」で動かしているから。ただし、ワタシの環境の場合基本 3.5 は「グローバルに可視ではない」んだけれど、python35.dll は正しく (見えてる 2.7 のではなく) 3.5 の標準ライブラリを読みに行く。これは動かして確認した。なんだろ、レジストリでも参照してんのかな。なので、「標準ライブラリもフルに同梱する」か、Python インストールを促す、という二択が決断の具体的な内容になるであろう。