LoginSignup
68
51

More than 5 years have passed since last update.

変数のスコープ

Posted at

Python の変数のスコープに関して誤解していて混乱したので勉強しなおしました。
(CPython の 2.7.10 と 3.4.3 で動作確認)

関数定義

関数定義はスコープを作るので、関数定義の中で宣言された変数は外からは見えない。

func_def.py
def func():
    i = 0
print(i)
% python2.7 func_def.py
Traceback (most recent call last):
  File "func_def.py", line 3, in <module>
    print(i)
NameError: name 'i' is not defined

% python3.4 func_def.py
Traceback (most recent call last):
  File "func_def.py", line 3, in <module>
    print(i)
NameError: name 'i' is not defined

基本ですね。この性質があるので、関数の外と中で意図しない影響の伝播が起きない。

クロージャ

関数定義中で定義されていない変数を参照している場合、関数定義と同じスコープに同名の変数があればそれに束縛されるので、呼び出し時の環境に同じ変数があっても影響を受けない。

closure.py
def getfunc():
    def func():
        return s
    s = "value@def"  # あえて後ろに書いてます
    return func
f = getfunc()
s = "value@call"
print(f())
% python2.7 closure.py
value@def

% python3.4 closure.py
value@def

lambda を使っても同様

closure_lambda.py
def getfunc():
    func = lambda: s
    s = "value@def"  # あえて後ろに書いてます
    return func
f = getfunc()
s = "value@call"
print(f())
% python2.7 closure_lambda.py
value@def

% python3.4 closure_lambda.py
value@def

未束縛の変数

関数定義時に束縛されなかった場合には、呼び出し時の環境を参照する。
意図しない影響の伝播が発生してしまう場合があるので注意が必要。

func_call.py
def getfunc():
    def func():
        return s
    return func
f = getfunc()
s = "value@call"
print(f())
% python2.7 func_call.py
value@call

% python3.4 func_call.py
value@call

for 文

for 文はスコープを作らないので、ループ変数も内部で宣言された変数も for 文の外側と共有される。

for.py
for i in range(3):
    s = "value inside loop"
print(i)
print(s)
% python2.7 for.py
2
value inside loop

% python3.4 for.py
2
value inside loop

C++ の for の動作(i はループ内部のみで参照できる)を想定していると間違うので注意

for.cpp
#include <stdio.h>
int main()
{
  const char *s = NULL;
  for (int i = 0; i < 3; i++) {
    s = "value inside loop";
  }
  printf("%d\n", i);
  printf("%s\n", s);
  return 0;
}
% clang for.cpp
for.cpp:8:18: error: use of undeclared identifier 'i'
  printf("%d\n", i);
                 ^
1 error generated.

リスト内包表記

リスト内包表記で使った変数が python2 では外に漏れてしまう。

参考:
* Python list comprehension rebind names even after scope of comprehension. Is this right?
* リスト内包表記と変数スコープ

list_comprehension.py
[i for i in range(3)]
print(i)
% python2.7 list_comprehension.py
2

これを知らなくて、プログラムミスをしてるのに何でエラーになってくれなかったのかとしばらく悩みました。
なお、python3 では漏れない。

% python3.4 list_comprehension.py
Traceback (most recent call last):
  File "list_comprehension.py", line 2, in <module>
    print(i)
NameError: name 'i' is not defined

応用?

変数のスコープが漏れちゃっているのを利用すると、 Python’s lambda is broken! に書かれている問題が起こる理由を見ることができる

lambda_in_list_comprehension.py
fs = [(lambda: i) for i in range(3)]
print(fs)
print(fs[0](),fs[1](),fs[2]())
i += 1
print(fs[0](),fs[1](),fs[2]())
% python2.7 lambda_in_list_comprehension.py
[<function <lambda> at 0x10fd67f50>, <function <lambda> at 0x10fd70050>, <function <lambda> at 0x10fd700c8>]
(2, 2, 2)
(3, 3, 3)

つまり、lambda 関数を3つ生成しているが、単一の i を束縛してしまっているということ。

set内包表記

set内包表記の場合は(リスト内包表記のように)変数が外に漏れたりはしない。

set_comprehension.py
{i for i in range(3)}
print(i)
% python2.7 set_comprehension.py
Traceback (most recent call last):
  File "set_comprehension.py", line 2, in <module>
    print(i)
NameError: name 'i' is not defined

% python3.4 set_comprehension.py
Traceback (most recent call last):
  File "set_comprehension.py", line 2, in <module>
    print(i)
NameError: name 'i' is not defined
68
51
0

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
68
51