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

Python VM

これ、前回に欲しいと思ってた、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 抽象構文

Python: ast (Abstract Syntax Tree: 抽象構文木) モジュールについて

Python とマクロ、インポートフックと抽象構文木

(Pythonによる簡単なLispインタープリタ実装方法(四則演算編))

魅力的なPython: SimpleParseモジュールを使った構文解析

PythonでScalaっぽいlambda式を書けるようにした

((Pythonで) 書く (Lisp) インタプリタ)

プログラミング言語 字句解析器(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で解剖してて、行き当たった、あの人の名前が出てきたぞ。

etc

Pythonの色々な話題

Python meta programming