前回の続き。
まず「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つの問題が「プロジェクトによっては問題にならない」ことは前回書いた。おさらい:
Python.Runtime.dll
をカレントディレクトリにコピーする以外の手段が見つかってない。
(Python.Runtime.dll
自身が別のものに依存しているからっぽいがよくわからん。)- ワタシの環境では Python 2.7 の
python27.dll
がグローバルに可視(「パスが通ってる」)ので 2.7 では動くが、Python 3.5 環境の方がそうなってないので、python35.dll
が見つからない、となって動かない。
どちらも、「皆に使ってもらいたい OSS をワシは作りたいんじゃぁ」な場合の「配布方法」ということで考えれば、Python.Runtime.dll
も python27.dll
(python35.dll
) も「同梱して配る」のが正解(ともにライセンス的に問題なし(PSF と MIT)) なので、何ら問題はない。
問題になる、というよりは、「コピーするしかない」を受け容れた場合に「いやな気分になる」のは、基本的には2つのケースである:
- オレさましか使わないローカルPCでしか使わないもん作っておる、のに、「あるのに、ある場所わかってるのに、それでもいちいちコピー祭りけ?」
- 絶賛開発中、の間や、もっとその手前の「検証実験をたこさんやり祭」時、「プロジェクトそのものをコピーするたんびにこれらを都度コピーするわけ?」
後者の例で「実験している場所を変えるたびに」Python.Runtime.dll
も python27.dll
(python35.dll
) をその場所にコピーせねばならんことを考えてみて欲しい。これは相当にウザい。
まず、Python.Runtime.dll
と python27.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.dll
か python35.dll
が検索パスから「一意に正しいものを見つけられる」環境にしてあれば(そういう環境変数設定にしていれば)、別に何もしなくてもいい。
で、残るは Python.Runtime.dll
なのだが…、やはりアンマネージドな DLL とは違うのね、アッセンブリを見つける仕組みは。「GAC に置かず、なおかつ自分と同じ場所に置かない」という場合のアッセンブリの解決方法が見つからない。自分の EXE や DLL と同じ名前の設定ファイル(今の場合「PyFromCSExam.exe.config
」)を書くと一定の制御が出来ることは発見したものの、例えば <probing> は目的のものに近いのに、「あんたの EXE や DLL のサブフォルダなら許してやるぜっ」てものなので、今やりたい「全然無関係の場所にある2つ」を結びつける手段にはならない。
GAC にないものだからねぇ。「コピーすべし」が唯一の解なのかもしらんなぁ。(も少し粘って調べてみるけれど、たぶんこれが答えなんじゃないかと今のところ思ってる。)