Inside python
前回 Pythonの新機能ってのを見た。その中でオイラーが注目したのは、数値の表現を 見やすく出来る機能。具体的には、100000 なんて指定をする時、100_000 のように 区切り記号として、アンダーバーを挟めるようになった事。
こういう大きな数って、ベンチをする時に回す回数を指定したりする時に出て来る。 やけに速いなと思ったら、ZEROが一つ足りなかったなんて、残念な失敗をしてる事が 多い。
こんな便利な機能が、なんで今頃やっと実装されたのか? オイラーのかすかな記憶では、 Perlはずっと昔から使えたように思うし、juliaのコードを読んでいた時にも、これを 実現するソースが混入してたぞ。(数字をパースする時、アンダーバーは単に無視する だけっていう、びっくりするぐらい簡単なやつ)
今回の改善は、入力だけではなく、出力時にも、10進数指定なら3桁区切り、16進数とかだと 4桁で区切りを入れられるようだ。これは助かるぞ。32Bitの16進数表現なんて読む気が しないもの。
そうそう、通販なんかで、電話番号とかカード番号を区切り無しで入力してくれってのに よく出会うけど、これって入力を間違うお前は馬鹿だと偉そうに構えているんだな。 ちょっとした親切心で、間違いが減るのにな。
10進数の時に3桁って、普通の欧米な人は、頭が悪くて数字は3桁のチャンクでしか覚えられないから? 日本人は昔から4桁区切りだよな。そう言えば、昔コンピュータって、8進数表現が 多かったな。
PDP-11とかオイラーが最初に触れたミニコンも、パネルのスイッチが3桁づつ色分けされてたな。あの頃は、コンピュータを扱う人も3桁区切りに異論が無かったんだな。
所で、大きな32Bitの整数が、4億だったか40億だったかすぐに思い出せない。
[ob: ~]$ echo '2^32' | bc 4294967296
御多分に漏れず、右からマウスのポインターを辿りながら、いち、じゅう、ひゃくと桁を 辿っているオイラーがいる。こういう時は、python3.6の出番かな? ブブー。
3桁区切りって、日本人には馴染まない。漢なら万進法に馴染んでいるはず。
[ob: ~]$ echo '2^32' | bc | rev | fold -w 4 | rev 7296 9496 42
これなら、2行目は万の桁、3行目は億の桁って事が一目両全ですよ。
[ob: ~]$ echo '2^32' | bc | rev | fold -w 3 | rev 296 967 294 4
これじゃ、チンプンカンプンです。欧米人の頭の中は、こうなっています。
[1]> (format nil "~r" 4294967296) "four billion, two hundred and ninety-four million, nine hundred and sixty-seven thousand, two hundred and ninety-six"
そして、Python頭もかたくなです。漢の表示は無視されてます。
>>> print( f'{4294967296:,d}' ) 4,294,967,296 >>> print( f'{4294967296:#_d}' ) 4_294_967_296 >>> print( f'{4294967296:#_x}' ) 0x1_0000_0000
Guido van Rossumに、 命数法を教えて、 10進4桁区切りをリクエストしようか。それとも、宝くじを当てて億万長者になるか、 FX博打をやってビリオネアになって、財団に寄付して、実装を強要しようか。
tokenize
前回の再掲。わざとエラーになるように仕向けたコード。
[debian tmp]$ cat spam.py a = {:_x}'.format(0xFFFFFFFF) [debian tmp]$ python spam.py File "spam.py", line 1 a = {:_x}'.format(0xFFFFFFFF) ^ SyntaxError: invalid syntax
見事に落ちた。この時、どんな風に文字列を認識してるか、確認出来る。
[debian tmp]$ python -m tokenize spam.py 0,0-0,0: ENCODING 'utf-8' 1,0-1,1: NAME 'a' 1,2-1,3: OP '=' 1,4-1,5: OP '{' 1,5-1,6: OP ':' 1,6-1,8: NAME '_x' 1,8-1,9: OP '}' 1,9-1,10: ERRORTOKEN "'" 1,10-1,11: OP '.' 1,11-1,17: NAME 'format' 1,17-1,18: OP '(' 1,18-1,28: NUMBER '0xFFFFFFFF' 1,28-1,29: OP ')' 1,29-1,30: NEWLINE '\n' 2,0-2,1: NL '\n' 3,0-3,0: ENDMARKER ''
注目は、架空の行にutf-8って字句が存在するって言ってる。名前と演算子に大きく分類 されるんだな。
[debian tmp]$ python -m tokenize -e spam.py 0,0-0,0: ENCODING 'utf-8' 1,0-1,1: NAME 'a' 1,2-1,3: EQUAL '=' 1,4-1,5: LBRACE '{' 1,5-1,6: COLON ':' 1,6-1,8: NAME '_x' 1,8-1,9: RBRACE '}' 1,9-1,10: ERRORTOKEN "'" 1,10-1,11: DOT '.' 1,11-1,17: NAME 'format' 1,17-1,18: LPAR '(' 1,18-1,28: NUMBER '0xFFFFFFFF' 1,28-1,29: RPAR ')' 1,29-1,30: NEWLINE '\n' 2,0-2,1: NL '\n' 3,0-3,0: ENDMARKER ''
コマンドにeオプションを追加すると、分類の所がもう少し細かい名前になる。これが、 Pythonが理解した、ソースの内容。後はこの種類によって、どんな事をユーザーが欲して いるか、pythonなりに理解してくって寸法だな。
[debian tmp]$ python -m tokenize spam.py 0,0-0,0: ENCODING 'utf-8' 1,0-1,1: NAME 'a' 1,2-1,3: OP '=' 1,4-1,11: STRING "'{:_x}'" 1,11-1,12: OP '.' 1,12-1,18: NAME 'format' 1,18-1,19: OP '(' 1,19-1,29: NUMBER '0xFFFFFFFF' 1,29-1,30: OP ')' 1,30-1,31: NEWLINE '\n' 2,0-2,1: NL '\n' 3,0-3,0: ENDMARKER ''
ちなみに、ちゃんと文法に則ると、上記のようになった。
disる
disるじゃなくて、逆アセンブルる。pythonには標準で付いてくる。
32.12. dis — Python バイトコードの逆アセンブラ
そして、簡単に実行出来る。こういうのrubyにもgaucheにも有るから、もうお馴染みだな。 普通のpythonは、自前で定義したスタックマシンで、演算とかはスタック上で行われる。 それじゃ遅いってんで、スタックレスマシンのpythonも発明されてたような。
[ob: tmp]$ python -m dis pg.py 1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (('ascii_lowercase', 'ascii_uppercase', 'digits')) 4 IMPORT_NAME 0 (string) 6 IMPORT_FROM 1 (ascii_lowercase) 8 STORE_NAME 1 (ascii_lowercase) 10 IMPORT_FROM 2 (ascii_uppercase) 12 STORE_NAME 2 (ascii_uppercase) 14 IMPORT_FROM 3 (digits) 16 STORE_NAME 3 (digits) 18 POP_TOP 2 20 LOAD_CONST 0 (0) 22 LOAD_CONST 2 (None) 24 IMPORT_NAME 4 (random) 26 STORE_NAME 4 (random) 4 28 LOAD_NAME 4 (random) 30 LOAD_ATTR 5 (seed) 32 LOAD_CONST 3 (987) 34 CALL_FUNCTION 1 36 POP_TOP : 10 96 LOAD_NAME 9 (print) 98 LOAD_CONST 6 ('') 100 LOAD_ATTR 10 (join) 102 LOAD_NAME 7 (sel) 104 LOAD_CONST 2 (None) 106 LOAD_CONST 7 (10) 108 BUILD_SLICE 2 110 BINARY_SUBSCR 112 CALL_FUNCTION 1 114 CALL_FUNCTION 1 116 POP_TOP 118 LOAD_CONST 2 (None) 120 RETURN_VALUE
trace
27.6. trace — Python 文実行のトレースと追跡
[debian tmp]$ python -m trace -t pg.py --- modulename: pg, funcname: <module> pg.py(1): from string import ascii_lowercase, ascii_uppercase, digits --- modulename: _bootstrap, funcname: _find_and_load <frozen importlib._bootstrap>(968): --- modulename: _bootstrap, funcname: __ini t__ <frozen importlib._bootstrap>(160): <frozen importlib._bootstrap>(161): --- mod ulename: _bootstrap, funcname: __enter__ : random.py(232): r = getrandbits(k) random.py(231): while r >= n: random.py(233): return r random.py(272): x[i], x[j] = x[j], x[i] random.py(269): for i in reversed(range(1, len(x))): pg.py(10): print(''.join(sel[:10])) Gk.853xq4O --- modulename: trace, funcname: _unsettrace trace.py(77): sys.settrace(None)
途中、モジュールの読み込みとかパースとかコンパイルをしたりして、やっとユーザーの目に つくrandom.pyとかpg.pyが出て来る。詳しすぎるトレースも、プチ困りもの。
そういう時は、下記を参照。他にも有用なオプションが有るぞ。
traceモジュールを使ってPythonプログラムの挙動を把握する
trace – 実行された通りに Python コードを追跡する
inspect
次は、そのスタックマシンの中を検査する機構。
29.12. inspect — 活動中のオブジェクトの情報を取得する
inGl1 = 8 inGl2 = 9 def testF(): inAt = 3 inAt2 = 4 def innerF(): global inGl1, inGl2 inInAt = 5 print( inAt + inGl1 + inGl2 + inAt2 + inInAt ) import pprint as pp;import inspect as ins;pp.pprint(ins.stack()) innerF() testF()
こんな風に、検査プローブを埋め込んで、それを実行するとな。
[ob: tmp]$ python z.py 29 [FrameInfo(frame=<frame object at 0x12829f734038>, filename='z.py', lineno=11, function='innerF', code_context=[' import pprint as pp;import inspect as ins;pp.pprint(ins.stack())\n'], index=0), FrameInfo(frame=<frame object at 0x12829f734c38>, filename='z.py', lineno=12, function='testF', code_context=[' innerF()\n'], index=0), FrameInfo(frame=<frame object at 0x128297d5c438>, filename='z.py', lineno=13, function='<module>', code_context=['testF()\n'], index=0)]
わざわざ綺麗に表示してねモジュール経由で出力してるけど無視されてる。そこで、ipythonですよ。
In [1]: run z.py 29 [(<frame object at 0x18185461f048>, '/tmp/z.py', 11, 'innerF', [' import pprint as pp;import inspect as ' 'ins;pp.pprint(ins.stack())\n'], 0), (<frame object at 0x181854620048>, '/tmp/z.py', 12, 'testF', [' innerF()\n'], 0), (<frame object at 0x18185460bd88>, '/tmp/z.py', 13, '<module>', ['testF()\n'], 0), (<frame object at 0x181816e55018>, '/usr/local/lib/python3.4/site-packages/IPython/utils/py3compat.py', 186, 'execfile', [" exec(compiler(f.read(), fname, 'exec'), glob, loc)\n"], 0), :
Ipythonの部分が見えているのは、ご愛敬。笑ってコラえて。(って、アッコじゃねぇよ) なお、この例は、下記から頂いてきました。
Inside Python
これ、前回に欲しいと思ってた、RHGならぬ、Python Hacking Guide ですよ。これを理解 出来れば、VMを理解出来たと思ってよい。
compile
In [2]: code = compile('a + 5', 'file.py', 'eval') In [3]: code.co_ code.co_argcount code.co_firstlineno code.co_name code.co_cellvars code.co_flags code.co_names code.co_code code.co_freevars code.co_nlocals code.co_consts code.co_kwonlyargcount code.co_stacksize code.co_filename code.co_lnotab code.co_varnames In [3]: code.co_code Out[3]: b'e\x00\x00d\x00\x00\x17S'
>>> with open('pg.py', 'r') as f: ... src = f.read() ... >>> code = compile(src, 'dmy.py', 'eval') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "dmy.py", line 1 from string import ascii_lowercase, ascii_uppercase, digits ^ SyntaxError: invalid syntax
複雑なのは駄目なのか、それともimport関係はコンパイラーの範囲外なのか。
importって、Python VM からしたら、BIOSみたいに、環境を整える下々の働きをする やつだから、そんなの知らんって事だろうか?
invalid syntax て言われてるんで、文法の範囲外って事。それにしては、disると、 IMPORT_FROM とか IMPORT_NAME が出てきてるんで、VMの特権命令だろう。jobコン が特別に計らってくれているのだろう。
>>> a = import random File "<stdin>", line 1 a = import random ^ SyntaxError: invalid syntax
お前は式じゃなくて、文だな。もっと言うと、制御文か。
ast
上で、ざっと実行系を見たけど、ソースをPython VMにかかるようにする、コンパイル系が まだだった。下記に資料を上げておく。(なお、一部、趣味に走っているので悪しからず。)
Python: ast (Abstract Syntax Tree: 抽象構文木) モジュールについて
(Pythonによる簡単なLispインタープリタ実装方法(四則演算編))
魅力的なPython: SimpleParseモジュールを使った構文解析
PythonでScalaっぽいlambda式を書けるようにした
プログラミング言語 字句解析器(lexer)と構文解析器(parser)
ドナルド・トランプ氏をイメージしたプログラミング言語「TrumpScript」が滅茶苦茶すぎる
pydoc
パイソン用の man と言うか、モジュール用の検索システム。
pydoc -k <keyword> Search for a keyword in the synopsis lines of all available modules. pydoc -b Start an HTTP server on an arbitrary unused port and open a Web browser to interactively browse documentation. The -p option can be used with the -b option to explicitly specify the server port.
[ob: t]$ pydoc split No Python documentation found for 'split'. Use help() to get the interactive help utility. Use help(str) for help on the str class. [ob: t]$ pydoc str.split Help on method_descriptor in str: str.split = split(...) S.split(sep=None, maxsplit=-1) -> list of strings Return a list of the words in S, using sep as the delimiter string. If maxsplit is given, at most maxsplit splits are done. If sep is not specified or is None, any whitespace string is a separator and empty strings are removed from the result.
これ、ちょっと不便。土地勘が無いと、たどり着けないぞ。キーワード検索しても 出てこないしね。
キーワードでモジュールを探すのは有り
[ob: t]$ pydoc -k random random - Random variable generators. secrets - Generate cryptographically strong pseudo-random numbers suitable for _random
それをやるなら、pydocのサーバーを起動して、そこで閲覧した方が早い。
sysconfig
sysconfigと言うモジュールで、Pythonの素性を知る事が出来る。だったら、pyconfigって名前が 素直で良いと思えるんだけど。
違うって、pythonさんは滅多に顔を出さないカーネルみたいなものだから、system相当なんよ。とにかく、色々な情報が出てくるので、絞りこむのが吉。
[debian ~]$ python -m sysconfig | grep work COVERAGE_INFO = "/home/ilan/minonda/conda-bld/work/Python-3.5.2/coverage.info" COVERAGE_REPORT = "/home/ilan/minonda/conda-bld/work/Python-3.5.2/lcov-report" PYTHONFRAMEWORKDIR = "no-framework" RESSRCDIR = "Mac/Resources/framework" RUNSHARED = "LD_LIBRARY_PATH=/home/ilan/minonda/conda-bld/work/Python-3.5.2" TESTPYTHON = "LD_LIBRARY_PATH=/home/ilan/minonda/conda-bld/work/Python-3.5.2 ./python" TESTRUNNER = "LD_LIBRARY_PATH=/home/ilan/minonda/conda-bld/work/Python-3.5.2 ./python ./Tools/scripts/run_tests.py" abs_builddir = "/home/ilan/minonda/conda-bld/work/Python-3.5.2" abs_srcdir = "/home/ilan/minonda/conda-bld/work/Python-3.5.2"
前回だったか、gdbで解剖してて、行き当たった、あの人の名前が出てきたぞ。