意外とややこしい Python のスコープを理解するためのクイズ14問

# イントロダクション

Python の変数のスコープ、参照のメカニズムは意外に直感的でない部分があり、初心者が罠にはまる可能性がある。しかし、一旦ルールを覚えればさほど複雑ではない。ここではその理解を助けるための問題を紹介する。

問題ごとに**何が出力されるか、もしくはエラーが出力されるかどうか**を答えよう。実行環境は Python 3 とする。難しい(というかマニアックな)問題は見出しが<font color="Red">赤色</font>になっている。

# 問題1

```python
x = 1

if True:
    x = 2

print(x)
```

## 解答

```
2
```

Python では `if` 文はスコープを形成しない。そのため `if` 文内の `x` は外の `x` と同一の変数となる。


# 問題2

```python
for i in range(10):
    x = i * 2

print(i, x)
```

## 解答

```
9 18
```

`if` と同様に `for` 文もスコープを形成しないので、ループ変数や内部で定義した変数はその `for` 文の後でもアクセスすることができる。

# 問題3

```python
ls = [x * 2 for x in range(10)]

print(x)
```

## 解答

```
(print(x) のとこで)
NameError: name 'x' is not defined
```

リスト内包表記(list comprehension)が実行されるとき、新たにスコープが作られそこでループ変数が定義される。そのため、外側のスコープに存在する `print(x)` からは `x` にアクセスできない。

# 問題4

```python
funs = []

for i in range(5):
    def f():
        print(i)
    funs.append(f)

for fun in funs:
    fun()
```

## 解答

```
4
4
4
4
4
```

関数 `f` の中から外側の変数 `i` を参照しているが、**`print(i)` が実行される時点での `i` の値が使われる**ことになる。5つの `f` が実行されるのはいずれも `for` 文の後であり、その時点では `i` は `4` であるため、すべて `4` を出力することになる。

なお、リスト内包表記についても同じことが起きる。

# 問題5

```python
x = 0

def f():
    x = 1

f()
print(x)
```

## 解答

```
0
```

関数 `f` が形成するブロック[^block]には `x = 1` という**代入文**が存在するため、このブロックのスコープには `x` という新しい変数が作られることになる。つまり、トップレベルの `x` は `f` によって変更されない。

[^block]: [ブロック](https://docs.python.org/ja/3/reference/executionmodel.html#structure-of-a-program)とはスコープ1つに対応するコードのまとまりであり、ブロックごとにスコープが形成される。ブロックを形成するのは関数、モジュール(つまりトップレベル)、クラス定義である。

# 問題6

```python
x = 0

def f():
    print(x)
    x = 1

f()
```

## 解答

```
(print(x) のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
```

1つ前の問題と同様に、関数 `f` が形成するブロックに `x = 1` という**代入文が存在する**ため、ブロックが実行される**前**にそのスコープに `x` という変数が作られる。しかし `print(x)` が実行される時点では `x` は値を持たない (unbound) であるため、例外が発生する。

# 問題7

```python
x = 0

def f():
    x += 1

f()
print(x)
```

## 解答

```
(print(x) のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
```

`+=` も代入文とみなされるため、1つ前の問題と同様に `f` が形成するブロックに変数 `x` が作られる。しかし、`x += 1` が実行されるとき `x` の値は存在しないため、例外が発生する。

外側のスコープの変数の値を変更するには `global` や `nonlocal` を使う必要がある。

# 問題8
```python
x = 0

def f():
    if False:
        x = 1
    print(x)

f()
```

## 解答
```
(print(x) のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
```

`if` 文はブロックを形成しないため、これも1つ前の問題と同様に `f` が形成するブロックに `x` への代入文が存在し、変数 `x` が作られる。しかし実際には `x` に値は代入されないため例外が発生する。

# <font color="Red">問題9</font>

```python
x = 0

def f():
    del x

f()
```

## 解答

```
(del x のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
```

実は `del` 文も代入文と同様に、そのブロックに変数を生成する効果がある。そのため、`f` が形成するブロックに変数 `x` が作られるが、`x` の値が存在しないまま `del x` が実行されるため例外が発生する。

このように変数を作る効果のある構文は代入文、`del` 文、`for` 文、クラス定義、関数定義、`import` 文、`with` と `except` の `as` がある。

# 問題10

```python
x = 0

def f():
    def g():
        print(x)
    x = 1
    g()

f()
```

## 解答

```
1
```

`g` が実行される時点では、一つ外側のスコープ (`f`) に `x` が存在し値が `1` であるため `1` が出力される。

# <font color="Red">問題11</font>

```python
x = 0

def f():
    x = 1
    del x
    def g():
        print(x)
    g()

f()
```

## 解答

```
(print(x) のとこで)
NameError: free variable 'x' referenced before assignment in enclosing scope
```

`del x` が実行されても `f` のスコープに `x` は存在しつづける(値がなくなるだけ)。そのため、`g` 内の `x` は `f` で定義された `x` を参照することになる。しかしその `x` は値を持たないので例外が発生する。

同じ理由で、以下の2つのコードでも同じ結果となる。

```python
x = 0

def f():
    x = 1
    def g():
        print(x)
    del x
    g()

f()
```

```python
x = 0

def f():
    def g():
        print(x)
    x = 1
    del x
    g()

f()
```

# <font color="Red">問題12</font>

```python
x = 1

def f():
    x = 2
    try:
        raise Exception()
    except Exception as x:
        pass

    def g():
        print(x)
    g()

f()
```

## 解答

```
(print(x) のとこで)
NameError: free variable 'x' referenced before assignment in enclosing scope
```

`try` 文で例外が発生し `except` 節が実行されると、その `as` で指定した変数は `del` と同様に**値が削除される**。そのため、`g` が実行される時点では `f` 内の `x` は存在するが値がないため例外が発生する。

なお、`except` 節が実行されなければ、該当する変数は削除されない。

# 問題13

```python
x = 1

class C:
    x = 2
    def f():
        print(x)
    f()
```

## 解答

```
1
```

クラス定義内のコードはスコープに関して特殊であり、そこで定義された変数(クラス変数)はそのスコープからしかアクセスできず、**クラス定義内の関数(つまりメソッド)からは直接アクセスできない**。


# <font color="Red">問題14</font>

```python
x = 0

def f():
    x = 1
    def g():
        print(eval("x"))
    g()

f()
```

## 解答

```
0
```

これは `eval` 関数の(直感に反する)振る舞いに関する問題である。`eval` 関数で実行したコードからアクセスできる変数は、実は**「グローバル変数」と「`eval` が実行されたブロックで定義された変数**」のみであり、その間で定義された変数にはアクセスできない。今のコードでは、`eval` 関数に渡されたコードからアクセスできる(ビルトイン以外の)変数は、1行目の `x` と `g` 内で定義された変数(1つもない)である。よってトップレベルの `x` が参照され、`0` が出力される。

なお、`exec` 関数も同様の振る舞いをする。

# 参考

- [4. Execution model — Python 3.8.0 documentation](https://docs.python.org/3/reference/executionmodel.html)
- [satwikkansal/wtfpython: A collection of surprising Python snippets and lesser-known features.](https://github.com/satwikkansal/wtfpython)