Last Updated: 2/6/2024, 5:44:57 AM

# スコープってなに?

スコープとは
名前空間をわける仕切り
です。

重箱の隅をつついているので 軽く読み流していただけると嬉しいです。 スコープの概念を少しでも明瞭にさせて、 次の記事「クロージャと nonlcal」につなげることを目標にしています。

Hello, world!

# 1. ざっくり全体像

スコープは、仕切られた範囲ようなものです。 簡単には Python のスコープは、3つに分けることができます。  built-in スコープ   global スコープ   local スコープ  です。

local, global, built-in の各スコープの名称については以下の PEP 227 に準拠します。

古い言語仕様 (2.0 ないしそれ以前のもの) は、名前を解決するために3つの名前空間を厳密に定義していました -- local, global, built-in という名前空間です。 ここで新たに nested スコープを追加することで、まだ束縛されていないローカルな名前解決を、 より外側の関数で囲まれた名前空間の中で、できるようにします。
The old language definition (2.0 and before) defines exactly three namespaces that are used to resolve names -- the local, global, and built-in namespaces. The addition of nested scopes allows resolution of unbound local names in enclosing functions' namespaces.
PEP 227 - Statically Nested Scopes (opens new window)

また、正確には4つで、もう1つ nested スコープというスコープがあるのですが。 これについては、次のページで見ていく予定ですが、現在加筆修正中です。

# 1.1. スコープの考え方

その範囲の中で定義された変数は、 「外から中」は  見ることもできない   触ることもできない  けど、 「中から外」なら  触ることはできない  けど  見ることならできます 

参照 変更
外から中 できない できない
中から外 できる できない



# 1.2. スコープの場所

local スコープは、関数定義文とクラス定義文の直下です。 global スコープは、スクリプトの直下です。 built-in スコープは、目に見える範囲にはありません。

#
# global スコープ
# 
def f():
    #
    # local スコープ
    #
#
# global スコープ
#
class C:
    #
    # local スコープ
    #
#
# global スコープ
#

C 言語などほかの言語だと関数定義文だけではなく for 文などにもスコープが与えられます。 よくこの違いが他言語から来られた方が戸惑う箇所です。 書籍「Readable Code」にも、Python の注意点として書かれていました。

ところで built-in スコープはどこにあるのでしょうか?  built-in スコープは、目に見えるところにはありません。  このあと、ご紹介いたします。

# 1.4. スコープの順番

狭いスコープの方が強いです。 参照の順番は local スコープ、次に global スコープ、次に built-in スコープを参照しています。 内側から外側に向けて名前を探していきます。 こういうのを難しい言葉で 名前解決 と呼ばれたりします。

変数と定数のことをまとめて名前と呼んだりします。

# 1.5. 確認問題

駆け足で概要を流しました。 いくつか問題を解いて、 まず global スコープと local スコープの挙動を確認していきたいと思います。

問題

実行結果1と等しいものはどれですか?

# コピペで実行できます。
a = 0
def f():
    print(a)

f()  # 実行結果1
  • 0
  • NameError が投げ返される。
問題

実行結果2と等しいものは、どれですか?

# コピペで実行できます。
def g():
    b = 1

g()
print(b)  # 実行結果2
  • 1
  • NameError が投げ返される。
問題

変数 list には、リスト型が代入されています。

list
>>> list
<class 'list'>
>>>

では、ここで問題です。 この変数 list に新しく別のリストを代入したらどうなるでしょうか? 実行結果 3 に正しいものを選択してください。

list = [0, 1, 2]
print(list)
# 実行結果 3
問題

実行結果 4 にはなにが表示されるでしょうか?

print(list)  # <class 'list'>

list = [0, 1, 2]
print(list)  # [0, 1, 2]

def f():
    list = 'Hello, world!'
    print(list)

f()  # 実行結果 4
問題

実行結果 5 にはなにが表示されるでしょうか?

print(list)  # <class 'list'>

list = [0, 1, 2]
print(list)  # [0, 1, 2]

def f():
    list = 'Hello, world!'
    print(list)

f()  # 'Hello, world!'

print(list)  # [0, 1, 2]

del list
print(list)  # 実行結果 5



外から中を触ることができない例も示したかったのですが、 いい例が思いつきませんでした。

ポイント
- 参照変更
外から中できないできない
中から外できるできない
built-in, global, local スコープ

# 2. built-in スコープ

built-in スコープとは Python 全体のスコープです。 min, max, sum と言った組み込み関数や、int, list, str と言った組み込み型は この built-in スコープに組み込まれています。

min, max, int, str なんて空気のように使っているので、 built-in スコープの中にはいっているという意識さえありませんでした。 以下2つの記事から知ることができました。ありがとうございます。

# 2.1. built-in スコープはどこにあるの?

Python の対話モード >>> を起動したいな、 あるいはスクリプトを実行したいなと思ったとき、 python コマンドを実行します。

このとき起動した Python が built-in スコープに、 「組み込み関数」や「組み込み型」の変数にオブジェクトを 裏でせっせと設定してくれています。

-v オプションを使って Python を起動するとたくさんの何かが裏で import されている様子が見れます(小文字のブイです)。 ただ具体的になにをしているかは、自分もわかりません。

python -v

-v (opens new window)
モジュールが初期化されるたびに、それがどこ(ファイル名やビルトインモジュール) から ロードされたのかを示すメッセージを出力します。

組み込み関数とは import しなくても使える関数です。 例えば min, max があります。 組み込み型とは import しなくても使える型(クラス)です。 例えば int, bool があります。

built-in スコープは、直接見ることはできせん。 built-in スコープは、組み込み関数または組み込み型の名前を保存しています。 built-in スコープは、頑張れば触ることができます。

# 2.2. __builtins__

built-in スコープにはいっているオブジェクトは、 __builtins__ の属性から参照することができます。 また新しい属性を追加することで built-in スコープに、 新しい名前を追加することができます。

__builtins__.min is min          # True
__builtins__.name = '岩倉玲音'   #            <- built-in スコープに直接名前を書き込む
name                             # '岩倉玲音' <- built-in スコープを参照している。

import builtins                  #            <- 標準ライブラリ builtins も合わせてご紹介致します。
assert builtins is __builtins__  # True       <- builtins と __builtins__ は同じものを指している。

このモジュールは Python の全ての「組み込み」識別子に直接アクセスするためのものです。 例えば builtins.open は組み込み関数 open() の完全な名前です。 ドキュメントは 組み込み関数 と 組み込み定数 を参照してください。
29.3. builtins — 組み込みオブジェクト (opens new window)

# 2.3. 実際に触ってみよう

ここで大事なのは Python 全体のスコープだということです。 built-in スコープに名前を追加することで、 スクリプト間で共通の名前を定義できてしまいます。

# Step 1.

まず sample.py に次のようなコードを書きます。

# sample.py
print(name)

# Step 2.

次に対話モード >>> を起動し sample.py を import します。

import sample
>>> import sample
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/sho/sample.py", line 1, in <module>
    print(name)
NameError: name 'name' is not defined
>>>

はじかれます。 なぜなら name は定義されていないからです。

# Step 3.

では変数 name を定義したらどうでしょうか?

name = 'こんにちは、世界!'
import sample
>>> name = 'こんにちは、世界!'
>>> import sample
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/sho/sample.py", line 1, in <module>
    print(name)
NameError: name 'name' is not defined
>>> 

これでもダメです。

# Step 4.

名前 name を定義していないのに import できるのでしょうか? __builtins__ の属性に代入すれば、できます。

__builtins__.name = 'Hello, world!'
import sample
>>> __builtins__.name = 'Hello, world!'
>>> import sample
Hello, world!
>>>

これはなにが起きたかというと sample.py の global スコープに name という変数がなかったので built-in スコープを参照した

# 2.4. でも使ってはいけない黒魔法です。

しかし、PEP 8 の from namespace import * が、 非推奨であると言われていることを考えると。 buitlins の属性にオブジェクトを直接代入する方法は、 絶対にぜーったいにやったらダメな方法だと言えるでしょう。

import - PEP 8 (opens new window)
ワイルドカードを使った import (from <module> import *) は避けるべきです。 なぜなら、どの名前が名前空間に存在しているかをわかりにくくし、コードの読み手や多くのツールを混乱させるからです。

グローバル変数をモジュール間で共有するにはどうしたらいいですか? - Python よくある質問 (opens new window)
単に設定モジュールをアプリケーションのすべてのモジュールにインポートしてください。

ポイント

built-in スコープが存在する。

# 3. global スコープ

モジュール全体で使う共通の値を定義したい時に使います。 global スコープは、 local スコープから見ることはできませんが、触ることはできるスコープです。

問題

実行結果 6 に近いものはどれですか?

a = 0  # global スコープ

def f():
    a = 1  # local スコープ

f()
print(a)  # 実行結果 6

# 3.1. global 文

global スコープは local スコープから触ることができません。 gloabl 文を使うことで触ることができるようになります。

問題

実行結果 7 に近いものはどれですか?

a = 0

def f():
    global a
    a = 1

print(a)  # 実行結果 7

# 3.2. globals 関数

globals 関数をもちいて global スコープを取得することができます。 ここで驚きなのは globals 関数で取得したオブジェクトは、辞書型 dict だということです。 つまり、スコープも内部的には辞書で実装されていたということです。

# 辞書を更新すると
globals()['a'] += 1

# 変数も更新される
a  # 2

# スコープは辞書型 dict
type(globals())  # <class 'dict'>

globals() Python 標準ライブラリ (opens new window)
現在のグローバルシンボルテーブルを表す辞書を返します。 これは常に現在のモジュール (関数やメソッドの中では、それを呼び出したモジュールではなく、それを定義しているモジュール) の辞書です。

# 3.3. 名前空間は目に見えるスコープ

しかし名前空間が目に見えるスコープとは、どういうことでしょうか? 以下のコードを見てください。 実際に Python の属性、さらには変数までもが(即ちスコープそのものが)、 辞書で実装されていたりします。

# スコープも属性も辞書でしかない

# global スコープ
#   var = 0
#
globals().update({'var': 0})
print(var)

#
# 属性
#   obj.attr = 1
#
class Cls:
    pass

obj = Cls()
obj.__dict__.update({'attr': 1})
print(obj.attr)
>>> print(var)
0
>>> 
>>> print(obj.attr)
1
>>> 

 スコープ (scope) とは、ある 名前空間 が直接アクセスできるような、 Python プログラムのテキスト上の領域です。  "直接アクセス可能" とは、修飾なしに (訳注: spam.egg ではなく単に egg のように) 名前を参照した際に、 その名前空間から名前を見つけようと試みることを意味します。
9.2. Python のスコープと名前空間 - Python チュートリアル (opens new window)

名前空間 (namespace) とは、名前からオブジェクトへの対応付け (mapping) です。 ほとんどの名前空間は、現状では Python の 辞書 として実装されています
9.2. Python のスコープと名前空間 - Python チュートリアル (opens new window)

覚える必要は一切ないのですが、 クラス名の文字列から動的にインスタンスオブジェクトを生成することができます。

class Cls:
    pass

Kls = globals()['Cls']
obj = Kls()

もっと簡単には eval 関数で取ってこれたりしますが...

class Cls:
    pass

Kls = eval('Cls')
obj = Kls()
ポイント

スコープと名前空間と辞書

# 4. local スコープ

# 4.1. 関数のローカルスコープ

何度か触れてきたので省略させていただきます。

# 4.2. クラスのローカルスコープ

関数定義文 def の中にローカルスコープがあるように、 クラス定義文 class の中にもローカルスコープがあります。 そしてこの class 定義文の中で定義された変数は、  クラス変数  と呼ばれます。

class Person:
    species = "Homo sapiens"

# 4.3. ネストするローカルスコープ

 クラスや関数は、それぞれ自分自身のローカルスコープを持ちます。  そしてローカルスコープはネストすることができます。 この次の記事ではネストしたスコープに焦点を当ててお話を進めていきます。

class Person:
    # 
    # Person クラスのローカルスコープ
    #
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender
        #
        # __init__ 関数のローカルスコープ
        #

person = Person('岩倉玲音', 14, 'Female')
person.name
person.age
person.gender
>>> person = Person('岩倉玲音', 14, 'Female')
>>> person.name
'岩倉玲音'
>>> person.age
14
>>> person.gender
'Female'
>>> 

# 4.4. locals 関数

ちなみに関係ありませんが、この local スコープも locals 関数で取得できます。 オススメはできませんが、こんなことができます。

# 対話モード >>> に
# コピペで実行できます。
class Person:
    def __init__(self, name, age, gender):
        self.__dict__.update(locals())
        print(locals())
        del self.self

person = Person('岩倉玲音', 14, 'Female')
person.name
person.age
person.gender
>>> person = Person('岩倉玲音', 14, 'Female')
{
 'self': <__main__.Person object at 0x1015d33c8>,  <- この self を del 文で消しました。
 'name': '岩倉玲音', 
 'age': 14,
 'gender': 'Female'
}
>>> person.name
'岩倉玲音'
>>> person.age
14
>>> person.gender
'Female'
>>> 

locals() - Python 標準ライブラリ (opens new window)
現在のローカルシンボルテーブルを表現する辞書を更新して返します。
Update and return a dictionary representing the current local symbol table.

自由変数は locals 関数によって関数ブロックの中で呼び出された時に返されますが、 クラスブロックの中で呼び出された時は自由変数は返されません。
Free variables are returned by locals() when it is called in function blocks, but not in class blocks.

モジュールの直下で呼び出した時は locals 関数は globals 関数と同じ辞書を返します。
Note that at the module level, locals() and globals() are the same dictionary.

 注釈 この辞書の内容は変更してはいけません; 変更しても、インタプリタが使うローカル変数や自由変数の値には影響しません。  

ポイント

関数のローカルスコープ

クラスのローカルスコープ

スコープに関する経験則

# 5. スコープは狭い方が良い

global スコープ, local スコープの
メリット、デメリット
書きやすさ 読みやすさ
global スコープ o x
local スコープ x o

一般にスコープは小さい方が良い、とされています。 global スコープは使わずに local スコープを使った方がよいということです。

local スコープを使うとは、関数間では「引数」と「返り値」を使ってオブジェクトをやり取りすると言えます。

# スコープが狭い
#   ローカル変数を使って値をやり取りする。
def add1(l):
    return l + 1

def mul2(l):
    return l * 2

x = 0
y = mul2(add1(x))
print(y)
>>> print(y)
2
>>> 

global スコープを使うとは、 関数間では「グローバル変数」を使ってオブジェクトをやり取りすると言えます。

# スコープが広い
#   グローバル変数を使って値をやり取りする。
def add1():
    global g
    g += 1

def mul2():
    global g
    g *= 2

g = 0
add1()
mul2()
print(g)
>>> print(g)
2
>>> 

これだけ短いコードだと、 メリット、デメリットがまったくハッキリしないのですが、 見ていきたいと思います。

# 5.1. 書きやすさ

local スコープは都度引数を渡さないといけないので、 global スコープに比べれば、書くのは煩雑になります。

上記のコードだと global 文がある分だけ、 global スコープを使ったものの方が、書くのが煩雑に見えてしまうのですが...

例えば Python のウェブアプリケーションフレームワークである Flask は、 グローバル変数を多用することで、とても簡単に描けるようになっています。

一般にグローバル変数は使わない方が良いとされていますが、 Flask は結果的にとても人気のあるフレームワークになりました。 そのあたりの Flask の背景については、以下にまとめました。

読んでいただく必要性は、まったくないのですが 複数の関数に複数の引数を与えて、 実行結果をテストし、実行時間を計測する小さいコードを書きました。

local スコープを使って 引数と返り値でひたすら渡し続けるのは正直辛かったのと、 個人で使うごく小さいツールだったので、妥協して global スコープを使いました。

# 5.2. 読みやすさ

個々の関数に区切って見た時に、 local スコープは何が操作対象になっているか明確です。 例えば引数とそこで関数の中で新たに作った 変数だけを意識しておけばよいです。 このような変数を難しい言葉で自由変数と言うそうです。

しかし global 変数が存在すると、 その存在を覚えておかないといけません。 あれ?この変数は、どこで定義されているんだっけ? というところから始めないといけません。

コードを読むこともさることながら、 コードの中から必要なことを探し出すことがとても面倒になります。 global スコープを使うというのは、 コードを global に把握しておかなければならなくなるのです。 その分だけ読む側の負担が大きくなります。

ローカルスコープを使う書き方は、 関数 a を呼び出す関数 b と呼び出された関数 a だけが関係を持ちます。 対してグローバルスコープを使う書き方は、 グローバル変数を使う全ての関数と関係を持ってしまいます。

グローバル変数の最悪なところは、そもそもどこから参照、変更されているかわからないことです。 結果的に意識しないといけない範囲が、その関数だけに止まらず、システム全体に広がってしまいます。

# ◯ いつ広いスコープを使っていいの?

では、いつ global スコープを使っても良いのでしょうか?

それはおそらく1度定義、すなわち import したら、変更することがない値、 すなわちイミュータブルな値は使ってもいいかなと思います。 それでも避けた方が良いに越したことはないと思うのですが。

例えば、Django, Flask などのウェブアプリケーションでは設定は、 モジュールのグローバル変数として、取り扱われています。

以下の記事は、スコープの広さについて、 経験に基づいて色々な視点から語ってくれていて。とても面白いです。

「変数のスコープは狭いほど良い」と妄信する - 分裂勘違い君劇場 by ふろむだ (opens new window)
変数でもメソッド名でもクラス名でも言えることだが、 単純に「スコープは狭いほどよい」という方針でプログラムすると、 逆に保守性も可読性も悪いプログラムができあがることがけっこうある*1。

# おわりに

ここまで以下のように見てきました。

以上になります。 ありがとうございました。