Python 用 lex/yacc の話のなかでしれーっと登場させちゃったが、せっかくなので。
Contents
Python for .NET の紹介
の前に
ワタシもかつてそうだったのでわかるのだが、「Microsoft がなんとなしに嫌い」という人々は結構多いと思う。単なる感情的な要因からのものもあるだろうし、エンジニアにとっては、「他の OS の主流から外れ過ぎていていやな記憶しかない」ということもあるだろう。
ただその好き嫌いも、「客観的な評価」を一度もしたことがないなら、少しくらいは評価してみようと思ってみても良いと思う。
.NET は Microsoft 世界のいわゆる一つの完成形なのだが、Unix/Mac ラバーズがその価値に気付くのは難しいかもしれない。よほど自発的に「評価してみよう」と思わない限り。
.NET 発生の「モチベーション」となった「問題」は、概ね2つだ。
一つ目の「問題」は伝統的な Unix CUI の一般的なプログラム間連携に見出すことが出来る。たとえばこんなだ:
1 me@host: ~$ ls -l | sed '1d' | awk '{ print $1, $6, $7, $8, $9}' | head
2 -rw-r--r-- Oct 25 10:15 aaa.txt
3 drwxr-xr-x Sep 16 17:16 somedir
4 -rw-r--r-- Sep 24 01:45 zzz.txt
コマンド ls
と awk
はお互いのことを知らない。だから awk
が「ls
の出力のうちファイル名はどやつだ」を知ることはないし、もっといえば ls
が一行目に「Total
」を出力するということも知りようがない。awk
はシンプルにテキストを受け取って「行ごとに指定の区切り文字で分解する」だけであり、分解されたフィールドを意味解釈するのは awk
ユーザの責務である。
この問題を言葉にすれば、「プログラム間連携のための標準語が存在しない」(もしくは貧相)、ということである。
二つ目の「問題」はもっとエンジニア寄りだ。すなわち、「オレさまアプリケーションや我が家のライブラリ」を開発する言語やターゲットの「位置」の問題である。以下 C 言語で書いたライブラリ:
1 int MYLIBRARY_calc_my_hoge()
2 {
3 return 0;
4 }
これを pure な java から使うことは出来ないし、「普通の手段で」C# から呼び出すことも出来ないし、VBScript から呼び出すことも出来ない。それらをするためには、各言語に用意された C API で「包んで」使う必要があるが、この「包む」が、「包んだライブラリを別途開発する」ことになるものも多く、例えば CPython の CPython C API がそうである。そしてこの C コードが「駅前のスタバ」にあるなら、自宅から呼び出すことは絶望的だ。
あーそうか、それで .NET が作られたのね? まぁ待て。そこに至るまでには長い道のりがあった。実はこの2つの大問題を解決するためのインフラでおそらく最古に属するものは、これは Microsoft とは関係ない CORBA だろうと思う。プログラム間連携のための共通語と位置透過性・バイナリ互換性を解消しようとするものだ。Microsoft が CORBA に触発されて開発した最初期のものが ActiveX だ。これは後に COM と名を変え、ネットワーク透過を加えた DCOM が登場した。.NET は DCOM の直接の子供であり、CORBA の曾孫と言える。
そういうわけで、「CORBA の価値を認めたことがある Unix 古株の Microsoft 嫌い」がいたら、「あーそういうことか」と思ってくれると面白いと思う。
「CORBA の価値を認めたことがある Unix 古株の Microsoft 嫌い」でない人たちへの説明としては…、そうね、「ActiveX のおかげで Microsoft Excel が公開する機能をプログラム(VBScript など)から使えるようになり、その流れからの集大成が .NET なのだだだ」てくらいかな。ただこういう説明だとさ、あんまし「どうオイシイのかワガラン」くない? まぁ一言では「どんな言語からでも位置が離れてても同じように使える」てことね。
よく .NET は「仮想マシン」であることから Java と比較されるが、実はベースになっているのは CORBA 的思想の方であって、いわば「CORBA + java的仮想マシン」が .NET だったりする。ね? なかなかっしょ?
Python から .NET 機能にアクセスする手段について色々
今回紹介したい Python for .NET 以外だと、安直なものからヘビィ過ぎるものまで色々ある。
PowerShell を呼び出す
最も安直(よく言えば素直)だがあまり採用したくない手段は、.NET シェルであるところの PowerShell を呼び出すことだ。それ自身が「シェル」なので、.NET そのものを使うこととは少し離れてしまうため、「遅い・重い」という問題を差し置いたとしても、「嬉しくない」と思うのが普通だ。
Python 用 lex/yacc の話 で例としてやっているのは Visual Studio プロジェクトファイルの解読(の一部)だが、Python for .NET 導入を決断する前は、実際これをやっていた。乗り換えとともに捨てたので未完成度甚だしいが、ただ捨てるのももったいない気もしたので、ここに貼り付けておく:
1 # -*- coding: utf-8 -*-
2 #
3 from __future__ import unicode_literals
4 from __future__ import absolute_import
5 from __future__ import print_function
6
7 import os
8 import sys
9 import re
10 import subprocess
11 import copy
12
13 import six
14
15
16 # ---------------------------------------
17 #
18 # Internal helpers
19 #
20 if sys.version[0] == '2':
21 str = unicode
22
23
24 def _encode(s):
25 if sys.version[0] == '2':
26 if hasattr(s, "encode"):
27 # python 2.x, and this string is 'unicode',
28 # if so, we have to encode for passing it
29 # to subprocess or spawn.
30 return s.encode(sys.getfilesystemencoding())
31 else:
32 if hasattr(s, "decode"):
33 # python 3.x, and this string is 'bytes',
34 # if so, we have to decode for passing it
35 # to subprocess or spawn.
36 return s.decode(sys.getfilesystemencoding())
37 return s
38
39
40 def _fix_env(env):
41 return {_encode(k): _encode(v) for (k, v) in env.items()}
42
43
44 def _lexer(s, tokdefs):
45 states = ["root"]
46 state = states[0]
47 while s:
48 for rgx, tok, trans in tokdefs[state]:
49 m = rgx.match(s)
50 if m:
51 yield tok, m.group(0)
52 if trans:
53 if trans == "#pop":
54 states.pop(-1)
55 state = states[-1]
56 else:
57 states.append(trans)
58 state = trans
59 s = s[m.span()[1]:]
60 break
61
62
63 _VARS_RGX = re.compile(
64 r"""
65 (
66 (?P<rtype>[$%])
67 \(
68 (?P<key>[\w.]+)
69 (?P<method>\.[\w]+\(
70 ((?P<args>[\w'`,\s]+))?
71 \))*
72 \)
73 )
74 """, flags=re.X)
75 def _accum_all_refs(v):
76 r"""
77 >>> print(
78 ... "\n".join(
79 ... _accum_all_refs(
80 ... "$(X)"
81 ... )))
82 X
83 >>> print(
84 ... "\n".join(
85 ... _accum_all_refs(
86 ... "$(X.Y)"
87 ... )))
88 X.Y
89 >>> print(
90 ... "\n".join(
91 ... _accum_all_refs(
92 ... "$(X.Y.Length)"
93 ... )))
94 X.Y
95 >>> print(
96 ... "\n".join(
97 ... _accum_all_refs(
98 ... "$(X.Y.SubString(1, 2))"
99 ... )))
100 X.Y
101 >>> print(
102 ... "\n".join(
103 ... _accum_all_refs(
104 ... "$(X.Y.SubString(1, 2))== $(ZZZ)"
105 ... )))
106 X.Y
107 ZZZ
108 >>> print(
109 ... "\n".join(
110 ... _accum_all_refs(
111 ... "[System.Text.RegularExpressions.Regex]::"
112 ... "Match($(Xx.Yy.SubString(3, 4)), `x`).Groups[0].Value")))
113 Xx.Yy
114 """
115 result = []
116 _STRING_PROPERTIES = [
117 "Chars",
118 "Length",
119 ]
120
121 m = _VARS_RGX.search(v)
122 s = v
123 while m:
124 span = m.span()
125 ref_key = m.group("key")
126 rk_cand, _, sprop = ref_key.rpartition(".")
127 if sprop in _STRING_PROPERTIES:
128 ref_key = rk_cand
129 result.append(ref_key)
130 s = s[span[1]:]
131 m = _VARS_RGX.search(s)
132 return result
133
134
135 # ---------------------------------------
136 #
137 # Private module variables
138 #
139 _TOKENS = {
140 "translate_from_msbuild_to_powershell": {
141 "root": [
142 (r"\s+", "WSP", ""),
143 (r"\$\((?!\[)", "START", "ref"),
144 (r"`", "QUOTE", "quote"),
145 (r"(==|!=|<=|>=|<|>|and|or)", "BOP", ""),
146 (r".", "REST", ""),
147 ],
148 "ref": [
149 (r"\(", "LPAR", "par"),
150 (r"\)", "END", "#pop"),
151 (r"`", "QUOTE", "quote"),
152 (r".", "REST", ""),
153 ],
154 "par": [
155 (r"\(", "LPAR", "par"),
156 (r"\)", "RPAR", "#pop"),
157 (r"`", "QUOTE", "quote"),
158 (r".", "REST", ""),
159 ],
160 "quote": [
161 (r"`", "QUOTE", "#pop"),
162 (r".", "REST", ""),
163 ],
164 },
165 }
166 for k in _TOKENS.keys():
167 _toks = _TOKENS[k]
168 for kk in _toks.keys():
169 for i, (rgxstr, tok, trans) in enumerate(_toks[kk]):
170 rgx = re.compile(rgxstr, flags=re.I)
171 _toks[kk][i] = (rgx, tok, trans)
172
173
174 # ---------------------------------------
175 #
176 # Exposed Helpers
177 #
178 def pydata_to_powershell_data(v):
179 r"""
180 >>> print(pydata_to_powershell_data(1))
181 1
182 >>> print(pydata_to_powershell_data(2**64))
183 18446744073709551616
184 >>> print(pydata_to_powershell_data(''))
185 ''
186 >>> print(pydata_to_powershell_data('""'))
187 '""'
188 >>> print(pydata_to_powershell_data('"aaa"'))
189 '"aaa"'
190 >>> print(pydata_to_powershell_data("'aaa'"))
191 "'aaa'"
192 >>> print(pydata_to_powershell_data([]))
193 {}
194 >>> print(pydata_to_powershell_data(["a", "b"]))
195 {'a','b'}
196 >>> print(pydata_to_powershell_data({"a": "b"}))
197 @{'a'='b'}
198 >>> print(pydata_to_powershell_data({"a": ["b", "c"]}))
199 @{'a'={'b','c'}}
200 >>> print(pydata_to_powershell_data({"a": {"b": "x", "c": "y"}}))
201 @{'a'=@{'b'='x','c'='y'}}
202 >>> print(pydata_to_powershell_data({"a": {"b": "x", "c": 3}}))
203 @{'a'=@{'b'='x','c'=3}}
204 """
205 # list -> {...,...}
206 # hashtable -> @{"..."="...",}
207 if isinstance(v, six.string_types):
208 return re.sub(r"""^[urb](["'])""", r"\1", repr(v))
209 elif isinstance(v, (tuple, list,)):
210 return "{{{}}}".format(
211 ",".join([
212 pydata_to_powershell_data(vi)
213 for vi in v
214 ]))
215 elif isinstance(v, (dict,)):
216 return "@{{{}}}".format(
217 ",".join([
218 "{}={}".format(
219 pydata_to_powershell_data(ik),
220 pydata_to_powershell_data(iv))
221 for ik, iv in sorted(v.items())
222 ]))
223 else:
224 return str(v)
225
226
227 def to_powershell_assign(name, value):
228 r"""
229 >>> print(to_powershell_assign("X", 1))
230 $X = 1
231 >>> print(to_powershell_assign("X", 2**64))
232 $X = 18446744073709551616
233 >>> print(to_powershell_assign("X", ''))
234 $X = ''
235 >>> print(to_powershell_assign("X", '""'))
236 $X = '""'
237 >>> print(to_powershell_assign("X", '"aaa"'))
238 $X = '"aaa"'
239 >>> print(to_powershell_assign("X", "'aaa'"))
240 $X = "'aaa'"
241 >>> print(to_powershell_assign("X", []))
242 $X = {}
243 >>> print(to_powershell_assign("X", ["a", "b"]))
244 $X = {'a','b'}
245 >>> print(to_powershell_assign("X", {"a": "b"}))
246 $X = @{'a'='b'}
247 >>> print(to_powershell_assign("X", {"a": ["b", "c"]}))
248 $X = @{'a'={'b','c'}}
249 """
250 return "${} = {}".format(
251 name,
252 pydata_to_powershell_data(value))
253
254
255 def translate_from_msbuild_to_powershell(command):
256 r"""
257 >>> print(translate_from_msbuild_to_powershell(
258 ... "[System.Text.RegularExpressions.Regex]::"
259 ... "Match(`x`, `x`).Groups[0].Value"))
260 [System.Text.RegularExpressions.Regex]::Match('x', 'x').Groups[0].Value
261 >>> print(translate_from_msbuild_to_powershell(
262 ... '$(Get-Item Env:XXX).Value'))
263 $(Get-Item Env:XXX).Value
264 >>> s = '''\
265 ... $([System.Text.RegularExpressions.Regex]::\
266 ... Match($(V), `(\d+)`).Groups[0].Value)'''
267 >>> print(translate_from_msbuild_to_powershell(s))
268 $([System.Text.RegularExpressions.Regex]::Match($V, '(\d+)').Groups[0].Value)
269 >>> s = '''\
270 ... $([System.Text.RegularExpressions.Regex]::\
271 ... Match($(V.X), `(\d+)`).Groups[0].Value)'''
272 >>> print(translate_from_msbuild_to_powershell(s))
273 $([System.Text.RegularExpressions.Regex]::Match($V.X, '(\d+)').Groups[0].Value)
274 >>> s = '''\
275 ... $([System.Text.RegularExpressions.Regex]::\
276 ... Match($(V.X.Replace(`1`, `3`)), `(\d+)`).Groups[0].Value)'''
277 >>> print(translate_from_msbuild_to_powershell(s))
278 $([System.Text.RegularExpressions.Regex]::Match($V.X.Replace('1', '3'), '(\d+)').Groups[0].Value)
279 >>> print(translate_from_msbuild_to_powershell("$(A) == ''"))
280 $A -eq ''
281 >>> print(translate_from_msbuild_to_powershell("$(A)==''"))
282 $A -eq ''
283 >>> print(translate_from_msbuild_to_powershell("$(A) != ''"))
284 $A -ne ''
285 >>> print(translate_from_msbuild_to_powershell("$(A) <= ''"))
286 $A -le ''
287 >>> print(translate_from_msbuild_to_powershell("$(A) >= ''"))
288 $A -ge ''
289 >>> print(translate_from_msbuild_to_powershell("$(A) < ''"))
290 $A -lt ''
291 >>> print(translate_from_msbuild_to_powershell("$(A) > ''"))
292 $A -gt ''
293 """
294 cnv = []
295 ref = []
296 for tok, s in _lexer(
297 command,
298 _TOKENS["translate_from_msbuild_to_powershell"]):
299 #
300 if tok == "START":
301 ref.append(s)
302 elif tok in ("LPAR", "RPAR"):
303 ref.append(s)
304 elif tok == "WSP":
305 cnv.append(" ")
306 elif tok == "BOP":
307 bop = {
308 "==": "###SP###-eq###SP###",
309 "!=": "###SP###-ne###SP###",
310 "<=": "###SP###-le###SP###",
311 ">=": "###SP###-ge###SP###",
312 "<": "###SP###-lt###SP###",
313 ">": "###SP###-gt###SP###",
314 "and": "###SP###-and###SP###",
315 "or": "###SP###-or###SP###",
316 }[s.lower()]
317 cnv.append(bop)
318 elif tok == "QUOTE":
319 if ref:
320 ref.append("'")
321 else:
322 cnv.append("'")
323 elif tok == "END":
324 ref.append(s)
325 tmp = "".join(ref)
326 if re.match(
327 r"^[A-Z0-9_][A-Z0-9_-]+\s",
328 tmp[len("$("):],
329 flags=re.I):
330 # if ref is like "$(Get-Item Env:Xxx)", don't change it.
331 cnv.append(tmp)
332 else:
333 # otherwise, change "$(Aaa)" to "$Aaa".
334 ref[0] = "$"
335 cnv.append("".join(ref[:-1]))
336 ref = []
337 elif tok == "REST":
338 if ref:
339 ref.append(s)
340 else:
341 cnv.append(s)
342 return re.sub(r"\s*###SP###\s*", " ", "".join(cnv))
343
344
345 # ---------------------------------------
346 #
347 #
348 #
349 def exec_single_command(
350 command,
351 props={},
352 env={},
353 merge_osenv_to_env=True,
354 from_msbuild=False):
355 r"""
356 Execute single powershell command. "command" must be
357 exactly one statement. For example, you can't pass
358 like "$A = 1 ; $A".
359
360 >>> print(exec_single_command(
361 ... "[System.Text.RegularExpressions.Regex]::"
362 ... "Match('x', 'x').Groups[0].Value"))
363 x
364 >>> print(exec_single_command(
365 ... "[System.Text.RegularExpressions.Regex]::"
366 ... "Match(`x`, `x`).Groups[0].Value", from_msbuild=True))
367 x
368 >>> print(exec_single_command(
369 ... "[System.Text.RegularExpressions.Regex]::"
370 ... "Match('x', $A).Groups[0].Value", props={"A": "x"}))
371 x
372 >>> print(exec_single_command(
373 ... "[System.Text.RegularExpressions.Regex]::"
374 ... "Match(`x`, $(A)).Groups[0].Value", props={"A": "x"},
375 ... from_msbuild=True))
376 x
377 >>> print(exec_single_command(
378 ... "$A.Length", props={"A": "xyz"}))
379 3
380 >>> s = "$A.Contains('xyz')"
381 >>> print(exec_single_command(
382 ... s, props={"A": "xyz"}))
383 True
384 >>> print(exec_single_command(
385 ... '$A.B', props={"A": {"B": "xyz"}}))
386 xyz
387 >>> print(exec_single_command( # N/F child key
388 ... '$A.B', props={"A": {}}))
389 <BLANKLINE>
390 >>> print(exec_single_command( # N/F parent key
391 ... '$A.B', props={}))
392 <BLANKLINE>
393 >>> print(exec_single_command(
394 ... '$A.B.Contains("xyz")', props={"A": {"B": "xyz"}}))
395 True
396 >>> print(exec_single_command(
397 ... '$B.Contains("xyz")', props={"A": {"B": "xyz"}}))
398 True
399 >>> print(exec_single_command(
400 ... '$(A.B.Contains(`xyz`))', props={"A": {"B": "xyz"}},
401 ... from_msbuild=True))
402 True
403 >>> fn = os.path.join(
404 ... os.path.dirname(sys.modules[__name__].__file__),
405 ... '__init__.py')
406 >>> print(exec_single_command(
407 ... "$([System.IO.File]::"
408 ... "ReadAllText('" + fn + "').SubString(0, 3))"))
409 # -
410 >>> env = {}
411 >>> env.update(os.environ)
412 >>> env["XXX"] = "YYY"
413 >>> print(exec_single_command(
414 ... '$(Get-Item Env:XXX).Value',
415 ... props={}, env=env))
416 YYY
417 >>> print(exec_single_command(
418 ... '$(Get-Item Env:XXX).Value',
419 ... props={}, env={"XXX": "YYY"}))
420 YYY
421 >>> print(exec_single_command(
422 ... "$(A) == 'xxx'", props={"A": "xxx"},
423 ... from_msbuild=True))
424 True
425 >>> print(exec_single_command(
426 ... "$(A) == '3'", props={"A": "3"},
427 ... from_msbuild=True))
428 True
429 >>> print(exec_single_command(
430 ... "$(A) == 3", props={"A": 3},
431 ... from_msbuild=True))
432 True
433 >>> print(exec_single_command(
434 ... "!!($(A) == 3)", props={"A": 3},
435 ... from_msbuild=True))
436 True
437 >>> exec_single_command(
438 ... "$(A) == 2 OR $(A) == 3", props={"A": 3},
439 ... from_msbuild=True)
440 True
441 >>> exec_single_command(
442 ... "$(A.Length) == 2 AND $(A) == '33'", props={"A": "33"},
443 ... from_msbuild=True)
444 True
445 >>> exec_single_command(
446 ... "'True'", props={"A": "33"},
447 ... from_msbuild=True) == 'True'
448 True
449 >>> exec_single_command(
450 ... "'True'", props={"A": "33"},
451 ... from_msbuild=True) is not True
452 True
453 >>> print(exec_single_command( # N/F parent key
454 ... '$A.B -eq $null', props={}, from_msbuild=False))
455 True
456 >>> print(exec_single_command( # N/F parent key
457 ... '$(A.B) == ""', props={}, from_msbuild=True))
458 True
459 >>> exec_single_command(
460 ... '"ABC".CompareTo("ABCD")', props={})
461 -1
462 """
463 # TODO: [MSBuild]
464 props_cp = copy.deepcopy(props)
465 if from_msbuild:
466 # powershell-like expression in msbuild allows
467 # the comparison beteween null value and blank
468 # sting like "$(A) == ''" even if variable A
469 # is not defined, but real powershell doesn't
470 # allow that. In powershell, if we want to
471 # do like that, we must use "$null" or
472 # "[string]::IsNullOrEmpty(...)", but in msbuild
473 # also allows like "$(A.SubString(0, 2)) == ''"
474 # even if variable A is not defined, in this
475 # case it is too hard to convert the expression.
476 # So, let's try to "default" properties that is
477 # referenced by the expression.
478 refs = _accum_all_refs(command)
479 for rk in refs:
480 k = rk.split(".")
481 td = props_cp
482 for i, ki in enumerate(k):
483 if ki not in td:
484 if i == len(k) - 1:
485 td[ki] = ""
486 else:
487 td[ki] = {}
488 td = td[ki]
489 command = translate_from_msbuild_to_powershell(
490 command)
491 command = "\r\n".join([
492 "$__RESULT = ({})".format(command),
493 "Try { $__RESULT.GetType().FullName }\r\n"
494 "Catch { '\r\n' }",
495 "$__RESULT"])
496 if merge_osenv_to_env:
497 env.update(os.environ)
498 if props_cp:
499 assigns = []
500 for k in props_cp.keys():
501 if isinstance(props_cp[k], (dict,)):
502 for ki in props_cp[k].keys():
503 assigns.append(
504 to_powershell_assign(ki, props_cp[k][ki]))
505 assigns.append(
506 to_powershell_assign(k, props_cp[k]))
507 if assigns:
508 command = "\r\n".join(["\r\n".join(assigns), command])
509 command = _encode(command)
510 try:
511 output = subprocess.check_output(
512 [
513 "powershell",
514 "-Command",
515 _encode(command),
516 ],
517 shell=False,
518 env=_fix_env(env),
519 stderr=subprocess.STDOUT).decode("mbcs").replace("\r\n", "\n")
520 if output[-1] == "\n":
521 # omit only *LAST* newline
522 output = output[:-1]
523 except subprocess.CalledProcessError as e:
524 # TODO: what should we do?
525 raise
526 res = output.split("\n")
527 #print(command.replace("\r\n", "\n"), res, "\n\n", file=sys.stderr)
528 assert len(res) == 2, res
529 if res[0] == "System.Boolean":
530 return res[1].lower() == "true"
531 if re.match(r"^System\.U?Int\d+$", res[0]):
532 return int(res[1])
533 return res[1]
534
535
536 if __name__ == '__main__':
537 import doctest
538 doctest.testmod()
.NET そのものでないだけでなく、PowerShell 構文と MSBuild 式が大きく異なっているために、その変換をごちゃごちゃやっている。あーやだやだ。
VBScript とか JScript を呼び出す
出来ると仮定すれば、PowerShell を呼び出すのとそう大差はないものの、おそらく PowerShell がマシである。PowerShell の方が .NET に「近い」。JScript などでは、.NET オブジェクトを構築するだけでもゴテゴテ書く必要がある、だろう。
「仮定」? ゴメンよ、やったこたぁねいのだ。VBScript、JScript は「COM テクノロジ」の方に寄り添って作られたもの。多分 VBScript.NET とか JScript.NET なんて名前を見かけるので、出来るんであろうけれど、これが「今の Windows では JScript == JScript.NET、だったらありがたい」とお祈りしてるくらいだから、よーするになんも知らん。のになぜ「PowerShell がマシに違いない」と思うのかといえば、JScript、VBScript 言語にとって「COM 連携は言語の拡張」だったから。要は「余分な手続きを踏まないと COM を使えない」だったから。
IronPython を使う
ちょうど java と python の jython に対応する、「Microsoft 製の、.NET で構築された Python」が IronPython であり、Iron シリーズの(記憶が正しければ)第一作目である。案外 IronRuby のほうが有名だったりしそうだけれども。
一度も使ったことがないのでわからんが、多分 .NET にフルアクセス出来るはず。それも「pythonic に」。
.NET ラッパーを CPython C API と clr で自作する
考えたくもない、が、CPython C API と、Visual C++ の clr 連携拡張を使えば、相当頑張れば書ける、と思う。ただし…。
distutils など標準 python 範疇での C のビルドは clr に関して無知なので、コーディングよりも先に、「どうやってビルドすんねん」だけで下手すれば何日も費やすこととなるだろう。
というかまさにそれをしてくれたのが Python for .NET だ。
そして Python for .NET
動機たるもの
IronPython などのオルタナティブ Python であってはならない、公式 CPython でないと困る、からだ、少なくともアタシにとっては。
公式 CPython でないと困る一番の理由は大抵は C 拡張に依存したライブラリだ。間接的にはつまり Cython で書かれたライブラリなんかもこれにあたる。
IronPython が受け容れられるなら Python for .NET はなくてもいいんじゃないかなと思う、多分。
ハロー過ぎるワールド
Python for .NET 公式サイトの最初の説明は歴史的な事情からか、ちょっと不親切な順番で始まる:
1 >>> from System import String
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 ImportError: No module named System
5 >>> #
古いバージョンでは自動ロードに頼っていたためにこれでインポート出来ていたらしい。
ワタシなんぞは最初からそうせい、と思ったりするが、他の言語から .NET 機能を使う場合と全く同じノリで、「アッセンブリ参照を解決する」という手続きが予め必要である:
1 >>> import clr
2 >>> clr.AddReference("System")
3 <System.Reflection.RuntimeAssembly object at 0x0000000002BA2278>
4 >>> import System
5 >>> s = System.String("abc")
6 >>> s.Substring(1)
7 'bc'
.NET モジュール・関数・プロパティ等々、すべて「普通の Python にみえる」。だから特殊なのは(多言語からの使用と同様)「アッセンブリ参照を追加する」部分だけだ。
アッセンブリは部分名で参照しようとすると衝突が問題になることがあるけど、完全名でもイケるんかな? ちょっとわからない。必要になりそうになったら調べよう…。
若干ハローじゃないワールド
「高等」てほどでもないが、ワタシが MSBuild 式を評価するのに使いたかったがために、つまり「文字列からこれらをコンストラクトしたり呼び出したりせねばならん」かったのね。別に Python for .NET に限った話ではないけれど、上述のアッセンブリの関係があるために、若干ややこしいのであった:
1 class _DotNetLoader(object):
2 def __init__(self):
3 self._clr = None
4 self._loaded_assemblies = {} # value: success or not
5
6 # value: imported assembly (as python module)
7 self._imported_assemblies = {}
8
9 def _import_clr(self):
10 if self._clr is None:
11 import clr # Python for .NET (pythonnet)
12 self._clr = clr
13
14 def load_assembly(self, asmn):
15 if asmn not in self._loaded_assemblies:
16 self._import_clr()
17 try:
18 self._clr.AddReference(asmn)
19 self._loaded_assemblies[asmn] = True
20 except Exception as e:
21 # actually it should be "System.IO.FileNotFoundException",
22 # but we can't assume it when no assemblies are loaded.
23 if "'System.IO.FileNotFoundException'" not in str(type(e)):
24 raise
25 # this nsref is not assembly (maybe module fullname)
26 self._loaded_assemblies[asmn] = False
27 return self._loaded_assemblies[asmn]
28
29 def import_assembly(self, asmn):
30 if asmn not in self._imported_assemblies:
31 self._import_clr()
32 try:
33 exec("import " + asmn)
34 except ImportError:
35 raise # what should we do?
36 self._imported_assemblies[asmn] = eval(asmn)
37 return self._imported_assemblies[asmn]
38
39 def get_function(self, name_with_ns):
40 # name_with_ns: like "[System.IO.Path]::Add"
41 self._import_clr()
42 ns, name = name_with_ns.split("]::")
43 ns_spl = ns[1:].split(".") # like "System", "IO", "Path"
44 asmn, modn = None, None
45 for i in range(len(ns_spl), 1, -1):
46 asmn = ".".join(ns_spl[:i - 1])
47 modn = ".".join(ns_spl[i - 1:])
48 if self.load_assembly(asmn):
49 break
50 asm = self.import_assembly(asmn)
51 return getattr(getattr(asm, modn), name)
52
53
54 _dotnetloader = _DotNetLoader()
55 _dotnetloader.load_assembly("System")
56 _dotnet_System = _dotnetloader.import_assembly("System")
get_function
つぅ具合に、関数・メソッドをダイレクトに取り出す公開部分しかないのは、こんな評価式にしか用事がないから:
1 "$([System.IO.Path]::Combine(`a`, `b`))"
これの場合、System.IO
が AddReference
に渡すもの、Path
がモジュール、だったかな、確か。
これをこのまんま使いたい人がいるともあんまし思えないけれど、まぁ使いたかったら使いなはれ。完全に正しいかは知らんよ。MSBuild で書ける限られたものでしか試してないもん。(それだけでなく「MSBuild 式の評価ライブラリ」自体が絶賛作り中であって、全然全貌網羅しとらん。)
↑からの派生ひとつ
「派生」がオブジェクト指向専門用語だと思いこんでいそうな人がいそうなんでコワイが、一般的日本語の意味での「派生」ね。
やはり同じく「MSBuild 式の評価ライブラリ」からのご用事。ちょっとした事情があって、「.NET の System.String」と Python 本体の文字列を同一視したかったのである。ついでに MSBuild に許される「ignore case」な凶悪な仕様にも耐える必要があった。ので、「isinstance(obj, (str,))
が真を返してもらわないと困る」兼「メソッド名の大文字小文字を問わない」の2つを施したヤツ:
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 import sys
8
9 if sys.version[0] == '2':
10 str = unicode
11
12 # ...
13
14 class String(str):
15 r"""
16 >>> s = String("abc")
17 >>> isinstance(s, (str,))
18 True
19 >>> isinstance(s, six.string_types)
20 True
21 >>>
22 >>> tab = [
23 ... # (lhs, rhs, expected)
24 ... # same length
25 ... ("A", "A", 0),
26 ... ("A", "a", 1),
27 ... ("a", "A", -1),
28 ... ("AAA", "AAA", 0),
29 ... ("AAA", "aAA", 1),
30 ... ("aAA", "AAA", -1),
31 ... #
32 ... # len(self) > len(rhs)
33 ... ("AAAa", "AAA", 1),
34 ... ("AAAa", "aAA", 1),
35 ... ("aAAa", "AAA", 1),
36 ... ("aAAaa", "AAA", 1),
37 ... #
38 ... # len(self) < len(rhs)
39 ... ("AAA", "AAAA", -1),
40 ... ("AAA", "aAAA", -1),
41 ... ("aAA", "AAAA", -1),
42 ... ("aAA", "AAAAA", -1),
43 ... ]
44 >>> for lhs, rhs, expected in tab:
45 ... result = String(lhs).CompareTo(rhs)
46 ... assert result == expected, str((lhs, rhs, expected, result))
47 ... #from ._powershell import exec_single_command
48 ... #expected2 = exec_single_command('"{}".CompareTo("{}")'.format(lhs, rhs))
49 ... #assert expected == expected2, str((lhs, rhs, expected2, expected))
50 ... #assert result == expected2, str((lhs, rhs, expected2, result))
51 >>>
52 >>> String("AAA").Contains("AAA")
53 True
54 >>> String(" AAA ").Contains("AAA")
55 True
56 >>> String("BBB").Contains("AAA")
57 False
58 >>> String("AAA").Contains(" AAA ")
59 False
60 >>>
61 >>> s = String("AAA")
62 >>> print(s.Replace("AAA", "BBB"))
63 BBB
64 >>>
65 >>> print(String("abc").Substring(0))
66 abc
67 >>> print(String("abc").Substring(0, 1))
68 a
69 >>> print(String("abc").Substring(1))
70 bc
71 >>> print(String("abc").Substring(1, 1))
72 b
73 >>> print(String("abc").Substring(1, 2))
74 bc
75 >>> print(String("abc").SubString(1, 2))
76 bc
77 >>>
78 >>> s = String("abc")
79 >>> s.Length
80 3
81 """
82 def __new__(cls, value):
83 # i believe that all methods in System.String are immurable.
84 # if it's dought, my emulator class is broken...
85 if isinstance(value, String):
86 obj = value
87 else:
88 obj = str.__new__(cls, value)
89 dnscls = getattr(_dotnet_System, "String")
90 obj._dotnetString = dnscls(value)
91 return obj
92
93 def __getattr__(self, name):
94 # Unfortunatelly, Microsoft always ignores its case...
95 for attr in [
96 x for x in dir(self._dotnetString)]:
97
98 if attr.lower() == name.lower():
99 return getattr(self._dotnetString, attr)
結果としてえれーシンプルに書けてるけど、実は密かにハマったのは内緒である。setattr
でメソッドを移送しようとしてた。これだと一見動くがなんかハングアップ的なことが起こった。何がダメだったのかわからずじまいだったが、この正解に気付いたのでそれ以上追ってない。
当然「isinstance(obj, (str,))
が真を返してもらわないと困る」はそんなにメジャーな要件ではないと思うが、ワタシの場合は「既存の 「isinstance(obj, (str,))
に頼った処理」内にしれーっとこの .NET String を混ぜ込む必要があったからに過ぎない。そうでないなら普通にダイレクトに Python for .NET の String を使えば良かろう。
String
以外でもこれやりたいのよねー。ただなぁ…、.NET もなんだかんだ C/C++ の子孫なので、「Uint32
」だのが Python とか Ruby みたいな「原則型なし(にみえる)」言語からは扱いずれーんだよね。
その他
ないよ、別に。もう「Python にしか見えないし .NET にしか見えない」んだもん、非常に素直にストレートに使える。「アッセンブリ」の件を除けば。
オススメ、と思うぞ。
2017-11-19追記: 「Python for .NET」という名前について
真っ先に書こうと思っていたのに完全に失念してた。
ワタシのご用がもとより「Python から .NET にアクセスしたい」だったわけなんだけれども、だとするとこの名前からは「違うよな多分」と思うはずでしょ? 欲しいのは「.NET for Python」であろう。
この疑問は無論すぐに浮かんだわけなんだけれど、公式ドキュメントを斜め読みして「なんだよ .NET for Python じゃん」と理解してこの記事になった。
そうなんだけど、やっぱこの命名、ヘンだよな、なんか書かれてないかしら、と思ったらあった:
名前から察するに、もともとこっちが目的だったのかもしれんね。だとしてもドキュメントが大々的に説明するのは「.NET for Python」側にはやっぱしみえるね。
ワタシは PowerShell をたまに使うくらいで基本的に .NET べったりの生活してないんで、当面は自分で試そうとは思わないけれど、まさに PowerShell から埋め込み Python てのも面白い可能性はあるね。簡単に出来るかはわからんけど。