13
6

More than 3 years have passed since last update.

Pythonの静的スコープはめちゃめちゃなのか?

Last updated at Posted at 2020-04-08

この間、『Why Python is not the programming language of the future』という素晴らしい記事を読みました。

その記事の中で、Pythonの欠点(Downsides of Python)として、「実行スピードが遅い」「ラムダ式がかなり制限されている」「モバイル開発にほとんど用いられない」などが挙げられていて、それらはかなり納得感のあるものだったのですが、「スコープに問題がある」という項目も書かれていました。

Originally, Python was dynamically scoped. This basically means that, to evaluate an expression, a compiler first searches the current block and then successively all the calling functions.

The problem with dynamic scoping is that every expression needs to be tested in every possible context — which is tedious. That’s why most modern programming languages use static scoping.

Python tried to transition to static scoping, but messed it up. Usually, inner scopes — for example functions within functions — would be able to see and change outer scopes. In Python, inner scopes can only see outer scopes, but not change them. This leads to a lot of confusion.

日本語で要約してみます。

  • Pythonでは当初動的スコープを採用していた
  • その後、Pythonは静的スコープに移行しようとしたが、めちゃめちゃになった(messed it up)
  • 通常、内部スコープは外側の変数を参照・変更できるが、Pythonでは参照のみでき、変更はできない

こちらは今まで私は把握できていなかった内容なので、少し調べてまとめてみました。私が調べた限り、これはPythonの実装が特別悪いのではなく、次のような事情があるようでした。

  • 例えばJavaScriptでは var 文でその関数のスコープの変数であることを指定するが、変数宣言の文が無いPythonではその手段が取れなかった
  • Pythonでは「nonlocal 文を追加する」という手段を取った

そのため、私が調べた限りでは、「内部スコープは外側の変数を参照・変更できるが、Pythonでは参照のみでき、変更はできない」という問題は、 nonlocal 文で解決しており、多少直感的でないにしても仕方ないことのようでした。ただ、それとは別の問題として nonlocal とは別に global が必要など、ごちゃごちゃしている面があるのは私も感じています。

Pythonと同様、静的スコープで変数宣言が不要な言語であるRubyでも同様の問題が発生していて、別の解決法が採用された(メソッドでは外部スコープの変数が参照できない、ブロックならできる)ようです。

「外側の変数を参照のみできる」とはどういうことか?

次のようなクロージャを使った例を考えます。JavaScriptでは、 createCounter 内部の関数が、外側のスコープの i を参照、変更しています。

// JavaScript
const createCounter = () => {
  var i = 0;
  return () => {
    alert(i);
    i++;
  };
};

const count = createCounter();
count();
// => 0
count();
// => 1
count();
// => 2

Pythonでは、 nonlocal を指定する必要があります。もし付けなかった場合は、存在しない変数 i に対して += の操作を行っているため UnboundLocalError: local variable 'i' referenced before assignment が発生します。

# Python
def create_counter():
    i = 0
    def _inner():
        nonlocal i
        print(i)
        i += 1
    return _inner

count = create_counter()
count()
# => 0
count()
# => 1
count()
# => 2

このように、参照であれば nonlocal を付けずに問題なく行えます。もはやカウンターではないですが…。

# Python
def create_counter():
    i = 0
    def _inner():
        print(i)
    return _inner

まとめると、「参照のみできる」というのは少し不正確で、実際はこうだと思います。

  • Pythonでは関数外の変数を参照はできる
  • 変更まで行う場合、 nonlocal 文を利用する必要がある

どのような経緯で実装されたのか

先程の記事で参照されている『What are the main weaknesses of Python as a programming language?』では、Jesse Tovさんからこのような回答が書かれています。

Python has perpetual scope confusion. As far as I can tell, this is because van Rossum didn't understand lexical scope initially, so he got it wrong. (This isn't a problem unique to Python. It seems pretty common among early versions of scripting languages.) Originally, Python was dynamically scoped, which everyone but RMS agrees is wrong. Then they did away with the dynamic scope, but made it so that inner scopes couldn't even see variables from outer scopes, which is bizarre in a supposedly block-structured language. Now inner scopes can see outer scopes but can't mutate them, which is bizarre in a supposedly object-oriented language. Some may claim this is a feature, but it's an accident of implementation. I'm sure when he fixes that, he'll break something else.

  • Pythonのスコープはずっと混乱してきた
  • 当初「動的スコープ」で実装され、次に「内部スコープから外部スコープの変数を参照できなくした」がこれはRMS(これってリチャード・ストールマンのことでしょうか?)以外は全員間違っていると判断した。次に参照のみ可能になった。
  • これは必然的な機能ではなく、これを修正すると他のなにかを壊してしまうだろう(と筆者は考えている)

公式のドキュメントでどのように議論が残っているのかも追ってみます。静的スコープに関連しているPEPは以下の2つのようです。

ちょうどこのスコープに関する議論が『コーディングを支える技術』にまとめられています。それによると、Pythonの静的スコープの実装の問題として、次のような2つの問題があったようです。

  1. 入れ子になったように見えるスコープが実際には入れ子になっていない(Pythonの歴史上混乱して解決してきたこと)
  2. ネストしたスコープの外側の変数を変更できない( nonlocal で解決したこと)

まず1つ目から触れていきます。本にあるサンプルコードを例に説明します。

# Python 2.x系のコードです
x = "global"

def foo():
    x = "foo"
    def bar():
        print x
    bar()
foo()

こちらのコードの実行結果はこう変わっていったようです。これが「内部スコープから外部スコープの変数を参照できなくした」と説明されているもののようですね。

  • Python2.0以前は「関数内のスコープで変数が見つからない場合、直接グローバルスコープを見に行く」ため"global"と表示されていた
  • Python2.1以降はネストしたスコープが参照され、"foo"と表示されるようになった

2つ目の「ネストしたスコープの外側の変数を変更できない」については、既に先に説明した通りです。

結局2006年に採用された方法は、関数冒頭で変数を「nonlocal」だと宣言するというものでした。この「nonlocal」という名前も、いくつもの候補の中から過去のコードでの出現頻度が最も低いものが選ばれました。

PEP3104でも確認できるのですが、「JavaScriptのように var をつけて宣言する」という案もあったようなのですが、次のように「シンプルで一貫性のある方法だが、過去のコードを全部置き換える必要がある」と考えて採用されなかったようです。

This proposal yields a simple and consistent model, but it would be incompatible with all existing Python code.

もし0から言語を実装するなら、宣言文があったほうがスコープについてはシンプルになりそうですね。

なぜglobalとnonlocalの2種類必要なのか?

JavaScriptでは var (関数スコープ)と宣言文なし(グローバルスコープ)の2種類であるため、Pythonでも nonlocal だけでいいんじゃないかという気がして調べてみました。

以下のようなコードも動作するのかと思いきや、 nonlocal i の箇所で SyntaxError: no binding for nonlocal 'i' found というエラーが発生してしまいます。

# iはグローバルスコープ
i = 1

def func():
    nonlocal i
    i = 2

func()
print(i)

このあたりは正直ややこしく感じます…。

また、 nonlocal が3.0からに対して、 global は2.0の時代から存在したようです。少し推測も混ざっていますが、以下のような時系列で実装されていったんじゃないでしょうか?

  • 外部の関数のスコープを参照できなかった時代に global 文が追加され、参照できるようになって nonlocal 文が追加された
  • そのためグローバルスコープは関数スコープとは違う実装がされており、 nonlocal ではなく global でアクセスする仕様が残ってしまった

もし、こちらについて詳しく知っている方がいたら教えてください。

まとめ

私が調べた結果、今のところ次のように考えています。

  • Pythonは現在の静的スコープになるまで、大幅な変更が加えられてきた
  • 内部スコープは外側の変数を参照・変更できるが、Pythonでは参照のみでき、変更はできない」という問題は(変数宣言文に比べて少しわかりにくいものの) nonlocal 文で解決している
  • globalnonlocal の2種類必要な理由ははっきりとは分からなかった(ので誰か教えてください!)
  • ただ、元記事が言うほど大きな欠点ではない気がする

ただ、もし見落としている箇所や「他の言語に比べてこういうケースで問題があるよ」という点があればコメントして頂けると嬉しいです。

参考記事

13
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6