Pythonではブロックを表すのにオフサイドルールが用いられている。
オフサイドルールってのはつや〜な言い方だが、要するにインデントを使ったブロック表記、と言う意味である。
ところで、プログラミング言語におけるブロックとは一体何だろうか。
Wikipediaなんかには次のように記述されている。
ブロック (英: block) とは、プログラミング言語におけるコードのまとまり(コードブロック)のことである。
文 (statement) から成る言語では、ブロックによって複数個(0/2個以上・言語により異なる)の文がまとまってひとつの文になっているものを複文 (compound statement) と呼ぶものもある。
なるほど、と。
しかし、モダンな言語に慣れた我々としては後述する次の文章の方が気になるのだ。
C言語などでは、ブロックは変数のスコープ(可視範囲)の区切りである。すなわち、あるブロック内で定義された変数には、ポインタ等によりエスケープされない限り(エスケープ解析を参照)、ブロック外からはアクセスできない。
そう、ALGOL以降のモダンな言語だと、通常、ブロックは変数のスコープを表す。ところがPythonはそうではない。割に古い形式を採用してるのがPythonなのだ。
どういう事なんだろうか。ANSI Cだとピンと来ないかもしれないが、例えばC99以降で次のような事を書いたらエラーが起きるだろう。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
for (int i = 0; i < 5; i++) {}
printf("%d\n", i);
return EXIT_SUCCESS;
}
forはブロックを形成する。従って、一見printfはi = 4で印字しそうに思えるのだがそうはならない。forと言うブロックで形成されたiと言う変数はブロック外からはアクセス出来ないのだ。
これは基本的に、ALGOL以降のレキシカルスコープを持った言語だと当たり前の動作となる。
例えばANSI Common Lispなんかでも
(defun foo ()
(loop for i in '(0 1 2 3 4))
(format t "~A" i)
0)
なんて書けば怒られる。iはloopが形作るブロックの中のみに存在し、やっぱり出力関数であるformatからはアクセス不可である。
ところが、Pythonだと違う。Pythonがdefでブロックを形成する際にはスコープが形成されるのに、forの場合にはスコープが形成されないのだ。
def foo():for i in range(5):passprint("{0}\n".format(i))return 0
実行例:>>> foo()
4
0
>>>
つまり、Pythonのブロックというのは一貫してないのだ。
ある時はスコープを形成するが別の場合にはスコープを形成しない。フツーは問題が起きないが、こういう風にツッコんでみるとボロが出る。
Pythonのおかしな実装、ってぇのは、スコープがキチンとしたレキシカルスコープになってない辺りである。
以下の2つのコードは全く同じ結果を返すが実はこの2つの内部的な動作は全然違う。
/* C言語版 */#include <stdio.h>
#include <stdlib.h>
int main(void) {
int j = 1;
for (int i = 1; i < 6; i++) {
j *= i;
}
printf("%d\n", j);
return EXIT_SUCCESS;
}
# Python版def foo():
j = 1
for i in range(1, 6):
j *= i
print("{0}\n".format(j))
return 0
一見、全く同じに見えるが、C言語版はforがブロックを生成していて、ここでjが使われてるがブロック内でjが定義されてるわけではない。従って、forブロックの外側にjを探しに行くわけである。
一方のPython版ではforはスコープを形成しない。従って関数foo内でのあらゆる変数は関数内だったらどこからでも参照出来るのだ。つまりここで定義されたローカル変数はレキシカルではない。非常に杜撰な実装だと思う。
まとめると、Pythonでは、
- ぶっちゃけ、関数定義時にしかスコープを形成しない
- スコープ内で使われる変数はレキシカルではない
となっている。
だから有名なアキュムレータの問題で、Pythonでは
def foo(n):
def bar(i):
n += i
return n
return bar
とは書けない。ローカル関数barはスコープを作るがこれは完全なレキシカルスコープではない。従って、bar内にいきなり登場したnは参照不可である。
Pythonでこの問題を解決するにはアドホックな手段に頼らないといけない。それがnonlocalと言うキーワードである。
def foo(n):
def bar(i):
nonlocal n
n += i
return n
return bar
こうすれば、barが作り出したスコープ特有のnではなく、外側のnだ、とPythonに知らしめる事が出来るわけだが・・・・・・。しかし、ぶっちゃけ、こういうアドホックなキーワードってみっともねぇよなぁ、とか思う。
やっぱり静的スコープとして自動で変数を探しにいくべきなんじゃないか。いつかその方向にPythonがなってくれるのを切に願っている。