Diary?

2007-09-29
Sat

(21:13)

Python: Myths about Indentation の翻訳。


Python をよく知らない人達の中には、 Python のインデントルールに対してかなりの先入観と誤った考えを持っている。ここでは、それらのいくつかについて書いてみようと思う。

「ホワイトスペースは Python のコードで重要な意味を持っている」

違う、普通はそうじゃない。文におけるインデントレベルだけが重要なんだ (つまりソースコードの左端のホワイトスペース)。他のどこであっても、ホワイトスペースは重要ではないし他の言語のように好きなように使えるんだ。同じように空行 (単なる空行か、ただのホワイトスペースだけの行) をどこにでも挿入できる。

また、互いに関連したネストされたブロック同士のインデントレベルの関係を除けば、インデントの正確な幅は重要じゃない。

その上、明示的あるいは暗黙の行継続を使った場合、インデントレベルは無視される。例えばリストを複数行にわたって分割して書く事が出来、インデントはまったく重要な意味を持たない。だから、次のようなコードを書く事ができる。

>>> foo = [
...            'some string',
...         'another string',
...           'short string'
... ]
>>> print foo
['some string', 'another string', 'short string']
 
>>> bar = 'this is ' \
...       'one long string ' \
...           'that is split ' \
...     'across multiple lines'
>>> print bar
this is one long string that is split across multiple lines

「Python は正確なインデントスタイルを強制する」

半分正解。まず、やろうと思えばブロックの内部をすべて一行で書くことが出来、よってインデントについて気にする必要はない。次の三つの if 文はどれも正当な書き方でまったく同じ動きをする (簡単のため、出力は省略)。

	>>> if 1 + 1 == 2:
...     print "foo"
...     print "bar"
...     x = 42
 
>>> if 1 + 1 == 2:
...     print "foo"; print "bar"; x = 42
 
>>> if 1 + 1 == 2: print "foo"; print "bar"; x = 42

もちろん、大抵の場合は最初の例のようにブロックを別々の行に分けて書きたいだろうけど、時には似たような if 文の塊があってそれぞれ一行で書いた方が便利な事もある。

もしもブロックを複数行に分けて書こうとするのなら、うん確かにそうだ、 Python はインデントルールについて従うように強制するね。要は「ブロック内部 (上の例の二つの print 文と代入文) は if 文自身よりもインデントされなければならない」ってことだけど、ぶっちゃけた話、他のやり方でインデントしたい? 僕はそう思わない。

だから結論はこうだ: 「Python は構造が不明瞭なコードを書きたいと思わない限り、今まで使ってきたインデントのやり方を使うように強制する」。あるいは「Python は馬鹿げたインデントによる不明瞭なプログラムの構造を許さない」。僕が思うに、それはとてもいいことだ。

こんな C/C++ のコードを見たことがあるかい?

/*  Warning:  bogus C code!  */
 
if (some condition)
        if (another condition)
                do_something(fancy);
else
        this_sucks(badluck);

ブレース (訳注: {} のこと) を使わない限り else は常にもっとも近い if に対して使われる。よって、このコードはインデントが間違ってるかバグってるかのどちらかだ。これは C/C++ の根本的な問題の一つだ。もちろん常にブレースを使うようにすれば何も問題はないけど、それは退屈な上にソースコードを肥大化させてしまうし、間違ったインデントによって偶発的にコードを不明瞭にしてしまうことを防げない。

Python ではインデントレベルと論理的なブロックは常に一致するので、そんな問題は起こりようがない。プログラムはインデントから期待される通りの動きを常にするんだ。

有名な著作家の Bruce Eckel 曰く:

Python においてはブロックがインデントによって表現されるので、 Python プログラムのインデントは一様になっている。そしてインデントは読み手からすると有意義なことだ。一貫したコードの書式により、私は他の誰かの書いたコードを読むことができ、そして「ああ、わかった。あの野郎、ここからここまでをブレースで括っていやがるな」なんてことでつまづき続けたりすることはなく、そんなことを考える必要はない。

「Python ではタブと空白を安全に混在させられない」

その通りだけど、そんなことしたくないだろう。正確には、 C なんかでもタブとスペースを安全に混在させられない。もしも C のソースコードをタブ幅の設定が違うエディタに移動した場合、それはきっと間違ってみえる (そして見た通りとは違った動作になる可能性がある)。

よって、インデントにタブと空白を混ぜないのは一般的によいことだ。もしもタブと空白のどちらかだけを使うのなら、それでいい。

さらに言えば、タブを使うこと自体をやめるのもいいことだろう。というのも、コンピュータの世界におけるタブのセマンティクスはしっかりと定義されておらず、異なるシステムやエディタではまったく異なって表示されることがある。さらに、タブはしばしばコピー & ペーストで間違って変換されたり破壊されたりする。

殆どのよいエディタはタブの透過的な変換とオートインデント/アンインデント機能をサポートしてる。タブキーを押せばエディタは次のインデント位置にいくのに必要なだけの空白 (タブ文字じゃないよ!) を (8 つかそれとも 4 つかあるいは望むだけ) 挿入し、そして別の何か (大抵バックスペース) で前のインデントレベルに戻ることができる。

言い換えると、それらのエディタはタブキーに求められる振る舞いをしつつも、空白だけを使うことでポータビリティを保っている。これは便利だし安全だ。

もしも自分が何をやってりうのかわかってるのなら、もちろんタブと空白を好きなように使っていし、他の人にソースコードをわたす前に "expand" みたいなツール (例えばこれは UNIX の場合) を使えばいい。もしもタブ文字を使うのなら、 Python はタブを空白 8 文字だとみなして動く。

「ただ単に気にくわない」

OK、嫌いになるのは勝手だし、多分そういう人は他にもいる。仮にインデントでブロックを表現する事が一般的ではなく慣れが必要だとみなされていたとしても、とても多くの利点があるんだ。もしも Python をマジにやり始めれば、本当にすぐに慣れてしまうさ。

ブロック終了の表現に endif みたいな (インデントでない) キーワードを使うことはできる。そういうのは Python のキーワードではないけれど、 Python には "end" を使ったコードを正しいインデントに直して end を取り去るツールがついてくる (訳注: 多分 http://svn.python.org/projects/python/trunk/Tools/scripts/pindent.py のこと。 Gentoo で Python を emerge しても付いてこなかったぞ)。これは Python コンパイラのプリプロセッサとして使うことができる。もちろん、本物の Python プログラマは誰も使ってないけどね。

「コンパイラはどうやってインデントレベルを解析してるの?」

解析の方法はちきんと定義されているし、それはとてもシンプルだ。基本的に、インデントレベルの変更はトークンストリームにトークンとして挿入される。

Python の字句解析器 (トークナイザ) はインデントレベルの保持にスタックを使っている。処理の開始時にはスタックには左端を表す 0 という値が入っている。ネストされたブロックが始まった場合、新しいインデントレベルがスタックにプッシュされ、そして "INDENT" トークンが構文解析器に渡されるトークンストリームに挿入される。一行当たりに存在できる "INDENT" トークンは一つだけだ。

上位のインデントレベルの行に当たった場合、新しいインデントレベルに等しい値が stack のトップにくるまでスタックから値がポップされる (none が見つかった場合、文法エラーになる)。値がポップされるたび、"DEDENT" トークンが作られる。見ての通り、一行あたり複数の "DEDENT" トークンが存在できる。

ソースコードの終わりに達したら、 0 だけがスタックに残るようになるまで "DEDENT" トークンが作られる。

次のサンプルコードを見てくれ:

>>> if foo:
...     if bar:
...         x = 42
... else:
...   print foo
... 

次の表の左にあるのが生成されたトークンで、右にあるのがスタックだ。

<if> <foo> <:>                    [0]
<INDENT> <if> <bar> <:>           [0, 4]
<INDENT> <x> <=> <42>             [0, 4, 8]
<DEDENT> <DEDENT> <else> <:>      [0]
<INDENT> <print> <foo>            [0, 2]
<DEDENT>                          [0]

気を付けてほしいのは、字句解析が終わった後 (構文解析が始まる前) には一切のホワイトスペースがトークンに残されていないということ (もちろん、文字列リテラルは別)。別の言い方をすれば、インデントレベルは字句解析器が扱うものであって構文解析器の扱うものではない。

それにより構文解析器は単に "INDENT" と "DEDENT" をブロックのデリミタとして扱える。これは C コンパイラのブレースの扱いその物だ。

上の例は簡単にしてあって、行の継続などもっとやらなきゃいけないことがある。それらもやはりきちんとした定義があって、興味があるならそれらはすべて (Python の完全で正式な文法を含む) Python Language Reference で読むことができる。


訳者後書き: ちょっと C/C++ の扱いというかブレースを使った言語の扱いが酷い気がするが、実際のところそうなんだから仕方がないか。ちょっと気を抜いてブレースを付け忘れてもコンパイラは怒ってくれないからなあ。ところで Bruce Eckel って「あんたRubyにいいたいことがあるんじゃないんですか? とくにPythonと比べてみて。」の人かな。

どーでもいいけど、物凄く簡単な字句解析のサンプルコードを Python で書いてみた。

def lex(lines):
  tokens = []
  stack = [0]
  for l in map(lambda x: x.replace('\t', '        ') ,lines):    
    level = count_indent_level(l)
    if level > stack[-1]:
      tokens.append('<INDENT>')
      stack.append(level)
    elif level < stack[-1]:
      dedent(stack, tokens, level)
    token = filter(lambda x: x,  l.strip(' ').split(' '))
    if token:
      print token
      tokens.extend(token)
  dedent(stack, tokens, 0)
  return tokens
 
def count_indent_level(line):
  level = 0
  if line.startswith(' '):
    for i in line:
      if i != ' ':
        break
      else:
        level += 1
  return level
 
def dedent(stack, tokens, level):
  while level != stack[-1]:
    stack.pop(-1)
    if not stack:
      raise Exception('syntax error!')
    tokens.append('<DEDENT>')
  return

次のようなファイルを与えると:

aaa bbb ccc
  ddd eee
    fff
  ggg hhh

次のような結果を返す。

['aaa',
 'bbb',
 'ccc',
 '<INDENT>',
 'ddd',
 'eee',
 '<INDENT>',
 'fff',
 '<DEDENT>',
 'ggg',
 'hhh',
 '<DEDENT>']

字句解析した結果はブレース使ってるのと変わらないってのがミソだな。

(22:31)

Python の glob の不思議。 glob() 関数を使った結果は大抵アルファベット順とかでソートされているのだけど (既にこの時点で怪しい)、そうなっていないときがある。不審に思ってマニュアルをよくよく読んでみると、 glob モジュールは実のところ os.listdir() と fnmatch.fnmatch() のラッパーみたいなもので、その os.listdir() は

Return a list containing the names of the entries in the directory. The list is in arbitrary order.

なんてことが書いてありやがった。まあそりゃそうか。

Creative Commons
この怪文書はクリエイティブ・コモンズ・ライセンスの元でライセンスされています。引用した文章など Kuwata Chikara に著作権のないものについては、それらの著作権保持者に帰属します。