Python for .NET の「Python for .NET」のほう、「.NET for Python」でなくて (わかったとこまで)

前回の続き。

まず「TypeError : long() argument must be a string or a number」問題:

 1 using System;  // for Console, etc.
 2 using System.Collections.Generic;  // for List
 3 using Python.Runtime;
 4 
 5 namespace PyFromCSExam
 6 {
 7     public class Exam
 8     {
 9         static void Main(string[] args)
10         {
11             using (Py.GIL())
12             {
13                 dynamic np = Py.Import("numpy");
14                 /*
15                 Console.WriteLine(np.cos(np.pi * 2));
16 
17                 dynamic sin = np.sin;
18                 Console.WriteLine(sin(5));
19 
20                 double c = np.cos(5) + sin(5);
21                 Console.WriteLine(c);
22                 */
23                 dynamic a = np.array(new List<float> { 1, 2, 3 });
24                 Console.WriteLine(a.dtype);
25 
26                 dynamic b = np.array(new List<float> { 6, 5, 4 }, dtype: np.int32);
27                 Console.WriteLine(b.dtype);
28                 /*
29                 Console.WriteLine(a * b);
30                 Console.ReadKey();
31                 */
32             }
33         }
34     }
35 }

これは pythonnet 本体の型の変換そのものが何か壊れているようで、今のところどうしようもなさそう。本体が改修されるのを待つしかないのではないかなと思う。これは pure Python な numpy だと:

1 import numpy as np
2 np.array([1., 2., 3.])
3 np.array([1., 2., 3.], dtype=np.float64)

のように ndarray 要素の型を「暗黙に」もしくは「明示的に」指定しているわけなのだけれど、「暗黙」の方は「落ちない」ものの b.dtype は「object」となって期待に反し、「明示」の方はランタイムエラーとなる。我々ユーザが関与出来ない部分で起こっていることなので、どうしようもない。

残る2つの問題が「プロジェクトによっては問題にならない」ことは前回書いた。おさらい:

  1. Python.Runtime.dllをカレントディレクトリにコピーする以外の手段が見つかってない。
    (Python.Runtime.dll 自身が別のものに依存しているからっぽいがよくわからん。)
  2. ワタシの環境では Python 2.7 の python27.dll がグローバルに可視(「パスが通ってる」)ので 2.7 では動くが、Python 3.5 環境の方がそうなってないので、python35.dll が見つからない、となって動かない。

どちらも、「皆に使ってもらいたい OSS をワシは作りたいんじゃぁ」な場合の「配布方法」ということで考えれば、Python.Runtime.dllpython27.dll (python35.dll) も「同梱して配る」のが正解(ともにライセンス的に問題なし(PSF と MIT)) なので、何ら問題はない。

問題になる、というよりは、「コピーするしかない」を受け容れた場合に「いやな気分になる」のは、基本的には2つのケースである:

  • オレさましか使わないローカルPCでしか使わないもん作っておる、のに、「あるのに、ある場所わかってるのに、それでもいちいちコピー祭りけ?」
  • 絶賛開発中、の間や、もっとその手前の「検証実験をたこさんやり祭」時、「プロジェクトそのものをコピーするたんびにこれらを都度コピーするわけ?」

後者の例で「実験している場所を変えるたびに」Python.Runtime.dllpython27.dll (python35.dll) をその場所にコピーせねばならんことを考えてみて欲しい。これは相当にウザい。

まず、Python.Runtime.dllpython27.dll (python35.dll) が違う、という話から。

前者は managed、後者は unmanaged。アンマネージド、なんて高尚な名前に思うかもしれんが、「.NET 前夜」はアンマネージドしかなかったのだ、だから「ふつーのレガシー C/C++ な DLL」つぅことである。

この「後者」は要するに Python.Runtime.dll が(C# による実装で)たぶん「[DllImport("python35.dll")]」して(たぶん)P/Invoke で C 実装をダイレクトに呼び出している(のだと思う)。この「[DllImport("python35.dll")]」(だと思うもの)の検索は、従来の DLL 検索と同じらしい。というところまではっきりわかった。ゆえ、

  • 2. ワタシの環境では Python 2.7 の python27.dll がグローバルに可視(「パスが通ってる」)ので 2.7 では動くが、Python 3.5 環境の方がそうなってないので、python35.dll が見つからない、となって動かない。

の方だけは環境変数を詐称すれば解決出来る:

 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# コードをここに書いてしまって
10     # ビルドも全部やってしまう。
11     import subprocess, os, io, sys
12 
13     # C# コード
14     #   公式サイト説明に欠けてるのは:
15     #       using Python.Runtime;
16     #   PythonEngine.Initialize(); がいるのかいらないのか、入ってなくても
17     #   動いた。CPython の C API のノリなら必要なのだが?
18     #
19     io.open("PyFromCSExam.cs", "w").write("""
20 using System;  // for Console, etc.
21 using System.Collections.Generic;  // for List
22 using Python.Runtime;
23 
24 namespace PyFromCSExam
25 {
26     public class Exam
27     {
28         static void Main(string[] args)
29         {
30             using (Py.GIL())
31             {
32                 dynamic np = Py.Import("numpy");
33 
34                 Console.WriteLine(np.cos(np.pi * 2));
35 
36                 dynamic sin = np.sin;
37                 Console.WriteLine(sin(5));
38 
39                 double c = np.cos(5) + sin(5);
40                 Console.WriteLine(c);
41             }
42         }
43     }
44 }
45 """)
46     # ビルド
47 
48     #     site-packages にある Python.Runtime.dll
49     import site
50     cands = site.getsitepackages()
51     for canddir in cands[1:]:
52         pythonengine_dll = os.path.join(canddir, "Python.Runtime.dll")
53         if os.path.exists(pythonengine_dll):
54             break
55 
56     #    pythonXX.dll の場所を PATH に追加しちまえよ
57     from distutils import sysconfig
58     pydlldir = sysconfig.get_config_var("BINDIR")  # unix系では LIBDIR のヤツ。
59     if sys.version_info[0] == 2:
60         def _fixenv(s):
61             return s.encode()
62     else:
63         def _fixenv(s):
64             return s
65     os.environ["PATH"] = _fixenv(";".join([
66         pydlldir]) + ";" + os.environ.get("PATH", ""))
67 
68     #    残念ながら Python.Runtime.dll をコピーしちゃいなよ
69     import shutil
70     shutil.copyfile(pythonengine_dll, os.path.basename(pythonengine_dll))
71 
72     #    ビルドしちゃいなよ
73     subprocess.check_call(
74         [
75             # csc がない、と言われる場合で「ほんとうは持ってる」場合は、
76             # BuildTools 2007 なら、スタートメニューから「x64_x86 Cross Tools
77             # Command Prompt for VS 2017」(等)を探して起動し、その環境から。
78             "csc.exe",
79             "-nologo",
80             "-target:exe",  # as console app
81             "-out:PyFromCSExam.exe",
82             "-reference:" + os.path.basename(pythonengine_dll),
83             "PyFromCSExam.cs",
84             ]
85         )
86     # 実行しちゃいなよ
87     subprocess.check_call(
88         [
89             "PyFromCSExam.exe",
90             ],
91         env=os.environ)

無論「PATH」の問題なので、ターゲットの python27.dllpython35.dll が検索パスから「一意に正しいものを見つけられる」環境にしてあれば(そういう環境変数設定にしていれば)、別に何もしなくてもいい。

で、残るは Python.Runtime.dll なのだが…、やはりアンマネージドな DLL とは違うのね、アッセンブリを見つける仕組みは。「GAC に置かず、なおかつ自分と同じ場所に置かない」という場合のアッセンブリの解決方法が見つからない。自分の EXE や DLL と同じ名前の設定ファイル(今の場合「PyFromCSExam.exe.config」)を書くと一定の制御が出来ることは発見したものの、例えば <probing> は目的のものに近いのに、「あんたの EXE や DLL のサブフォルダなら許してやるぜっ」てものなので、今やりたい「全然無関係の場所にある2つ」を結びつける手段にはならない。

GAC にないものだからねぇ。「コピーすべし」が唯一の解なのかもしらんなぁ。(も少し粘って調べてみるけれど、たぶんこれが答えなんじゃないかと今のところ思ってる。)