0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

演習形式で学ぶPythonプログラミング vol.19 ~グローバル変数とローカル変数~

Posted at

まえがき

Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。

対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。

ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。

今回は「関数の設計と記述スタイル」について扱います。それでは始めましょう!

Q.19-1

グローバル変数・ローカル変数・nonlocal変数がすべて定義された関数を読解する際に、変数のスコープを正しく把握するための設計的指針を示せ。Python におけるスコープルールをLEGB(Local → Enclosing → Global → Built-in)として図解し、それぞれの具体的な対応箇所を説明せよ。

問題背景

LEGMの概念図と対応

Pythonは、以下の順に変数を探索し、最初に見つかった定義を用いる。

LEGB概念図
┌────────────── Built-in ──────────────┐
│  組み込み定数や関数(例: len, sum)    │
├────────────── Global ───────────────┤
│  モジュールレベルの変数(global)     │
├───────────── Enclosing ─────────────┤
│  外側関数のローカルスコープ(nonlocal)│
├────────────── Local ────────────────┤
│  現在の関数のスコープ(local)         │
└──────────────────────────────────────┘
スコープ 特徴 設計指針
Local 最も内側の関数のスコープ。代入があると暗黙的にローカル変数となる 処理の局所化。関数内で完結する値は必ずローカルとする
Enclosing 外側の関数のスコープ(クロージャ) 非ローカル変数を明示する nonlocal を使って状態変更を可視化
Global モジュールレベルのスコープ global 宣言により明示的にアクセス。使用箇所を限定する
Built-in Pythonに組み込まれた関数や定数 上書きを避ける(例:sumlist を変数名に使わない)

解答例とコードによる実践

# 【1】グローバル変数(Globalスコープ)
g_counter = 0  # 【1】モジュール全体で共有される変数。関数内で global 宣言で操作可能。

# 【2】外側の関数を定義(Enclosingスコープ)
def outer_function():  # 【2】Enclosingスコープを作る外側の関数。

    msg = "enclosing message"  # 【3】nonlocal 宣言で内側関数からアクセス可能な変数。

    # 【3】内側関数(Localスコープ)を定義
    def inner_function():  # 【4】ローカルスコープを持つ内側関数。

        global g_counter  # 【5】グローバル変数への明示的なアクセス。
        nonlocal msg      # 【6】外側スコープ(enclosing)の変数にアクセス・変更する宣言。

        local_var = "I'm local"  # 【7】inner_function 内部だけで有効なローカル変数。

        g_counter += 1  # 【8】グローバル変数 g_counter をインクリメント。
        msg += f" -> call{g_counter}"  # 【9】nonlocal変数 msg に追記(状態保持)。

        print("local_var:", local_var)  # 【10】ローカル変数の出力。
        print("nonlocal msg:", msg)     # 【11】Enclosingスコープの変数の出力。
        print("global g_counter:", g_counter)  # 【12】グローバル変数の出力。
        print("built-in len('abc'):", len('abc'))  # 【13】Built-in関数の使用例。

    return inner_function  # 【14】inner_function を返してクロージャを作る。

# 呼び出し部とテスト実行
# 【15】outer_function を呼び出して inner_function を取得
f = outer_function()  # 【15】この時点で msg = "enclosing message" がクロージャ内に保存される。

# 【16】関数 f(inner_function)を複数回呼び出し、状態変化を確認
f()  # 【16】1回目の呼び出し → g_counter = 1, msg も更新
f()  # 【17】2回目の呼び出し → g_counter = 2, msg がさらに更新  
実行結果
local_var: I'm local
nonlocal msg: enclosing message -> call1
global g_counter: 1
built-in len('abc'): 3
local_var: I'm local
nonlocal msg: enclosing message -> call1 -> call2
global g_counter: 2
built-in len('abc'): 3

上記のコードにおいて、LEGB対応は以下

スコープ 変数名 定義箇所 アクセス箇所
Local local_var inner_function print("local_var:", ...)
Enclosing msg outer_function nonlocal 宣言の上で更新
Global g_counter モジュール直下 global 宣言の上で更新
Built-in len Python標準ライブラリ print("built-in len('abc')")

設計的指針:スコープ混在関数を読解・設計するための原則

原則 理由
スコープごとに変数名を変える 変数名が重複しているとスコープ誤認を招くため
global/nonlocal 宣言は関数冒頭にまとめる 読解者が関数の副作用スコープを最初に把握できるようにする
スコープに関わる変数の初期値は目に見えるようにする 状態の初期化が暗黙的だと、変数がいつ・どう変わるかが不透明になる
状態変化を伴う処理は関数化・モジュール化する グローバルやnonlocal変数に依存しない設計の方が安全である

補足:LEGBルールのトラブル防止ポイント

・関数内で x = 5 のように代入があれば、Pythonは暗黙的に x をローカル変数と解釈する。
・グローバルに同じ名前の変数があっても、参照前に代入があると UnboundLocalError になる。
・そのため、明示的な globalnonlocal 宣言が必要になるケースを把握しておくことが重要である。

Q.19-2

Python の関数内において、条件分岐やループを通じて定義された変数が予期せぬ値を持ちうる場面に対して、安全な変数設計のルールを提案せよ。

問題背景

【1】 Python のスコープと変数定義の特性

Python では、関数内で変数に代入が行われた時点でその変数はローカルスコープに属するとみなされる。ただし、その代入が条件文やループの内部にしか存在しない場合、そのブロックが実行されなければ変数が定義されない状態となる。その結果、関数内でその変数を使用しようとすると UnboundLocalError が発生することがある。

【2】 典型的なエラー構造とリスク

以下のような構造は実行パスによっては変数 result が定義されずに使われてしまう:

def calculate(x):
    if x > 0:
        result = x * 2
    return result  # x <= 0 のとき result は未定義 → エラー

このような構造は、条件分岐に依存した変数定義が安全に行われていない例である。

【3】 よくある場面とリスク

構造 リスク内容
if 分岐内のみで代入 条件を満たさない場合は変数が定義されない
for 文内のみで定義 ループが0回転の場合に変数が定義されずに参照される
try 節内でのみ定義 except が発生すると変数定義が飛ばされる
動的な条件による代入 値が型不一致やNoneになるパターンの考慮不足

設計上の安全ルール

・関数の先頭で初期値を明示する:すべての変数にデフォルト値を与える
・変数のスコープを限定する:なるべく iffor の外で使用しない
・使用前に存在を保証する構造にする:elsetry...except を用いて確実に代入
・関数を小さく保つ:スコープ混在による読み間違いを防ぐため、単責任に分離する

解答例とコードによる実践

【1】スコアに応じてランク(S〜Dまたは未評価)を返す関数を定義する

def evaluate_score(score):
rank = "未評価" # 【2】すべての分岐に当てはまらない場合のために初期値を定義

if score >= 90:  # 【3】スコアが90点以上のとき
    rank = "S"  # 【4】最上位ランクを代入
elif score >= 75:  # 【5】スコアが75〜89の範囲なら
    rank = "A"  # 【6】Aランクを代入
elif score >= 60:  # 【7】スコアが60〜74の範囲なら
    rank = "B"  # 【8】Bランクを代入
elif score >= 40:  # 【9】スコアが40〜59の範囲なら
    rank = "C"  # 【10】Cランクを代入
elif score >= 0:  # 【11】スコアが0〜39の範囲なら
    rank = "D"  # 【12】Dランクを代入

return rank  # 【13】最終的なランクを返す(常に何らかの値が入っている)

【14】テスト対象となるスコアのリストを定義する(異常値 -5 を含む)

scores = [95, 80, 65, 45, 20, -5]

【15】各スコアに対してランクを評価し、結果を表示するループ

for s in scores:
# 【16】評価結果を整形して表示(f文字列で可読性を高める)
print(f"Score: {s} → Rank: {evaluate_score(s)}")

```: 実行結果
Score: 95 → Rank: S  
Score: 80 → Rank: A  
Score: 65 → Rank: B  
Score: 45 → Rank: C  
Score: 20 → Rank: D  
Score: -5 → Rank: 未評価

補足:ループによる未定義の典型エラー例と改善

def compute_sum(values):  # 【1】
    total = 0  # 【2】0件でも安全なよう初期化
    for v in values:  # 【3】
        total += v  # 【4】
    return total  # 【5】常に定義されている

print(compute_sum([]))  # 【6】→ 0(安全)

本問のまとめ

Pythonでは、条件分岐やループによって定義される変数が、意図せず未定義状態になることで実行時エラーが発生するリスクがある。

この問題に対しては以下のような安全設計が有効である:

安全設計ルール 説明
初期値を必ず与える rank = Nonetotal = 0 などで未定義状態を避ける
条件ブロック外でも変数が使える設計にする if-else を必ず網羅することで常に値が設定されることを保証
スコープを意識して関数を分割する 状態をまたぐ変数が多い場合は関数の分離で複雑度を軽減
テストで全経路を確認する 予期しない None や未定義が発生しないよう、全条件分岐をテストする

Q.19-3

ブロック内でのみ使うべき変数にグローバルスコープが割り当てられてしまった場合に発生する設計的な不整合とその回避法を記述せよ。

問題背景

【1】Pythonにおけるスコープの単位

Pythonでは、C言語やJavaと異なり、if 文や for 文などの制御ブロックがスコープの境界とはならない。つまり、ブロック内で定義された変数もそのブロックを抜けた後で依然として有効である。

if True:
    temp = 123  # これは if 文の外でも有効になる

print(temp)  # → 123

問題の本質:スコープの設計と責務の不一致

ブロック内でのみ使用すべき一時変数(例:ループ変数、補助値)に対して、誤ってグローバルスコープで定義されてしまうと、以下のような設計的な不整合が発生する:

問題点 説明
意味的な責務の逸脱 一時変数が関数外に存在するのは責務違反である
再定義や上書きのリスク 同じ名前を別用途で再利用した際に意図せぬ衝突や上書きが発生する
予期せぬ副作用やバグの原因 他の関数や処理に影響を及ぼす可能性がある

解答例とコードによる実践

設計的な不整合の発生例

# 【1】グローバルスコープに誤って設置された変数
temp = "外部値"  # 【1】この変数は本来関数外で使うべきではないが、定義されてしまっている

def calculate_total(items):  # 【2】合計を計算する関数
    total = 0  # 【3】初期合計値を定義

    for item in items:  # 【4】リストをループ処理
        global temp  # 【5】temp をグローバル変数として扱う宣言(ここが不整合の起点)
        temp = item * 2  # 【6】本来はループ内部の一時値で済む内容だが、グローバルに保持される
        total += temp  # 【7】temp を合計に加算

    return total  # 【8】最終合計を返す

# 【9】計算処理を呼び出す
result = calculate_total([1, 2, 3])  # 【9】1*2 + 2*2 + 3*2 = 12 を想定

# 【10】結果を表示
print("合計:", result)  # 【10】12 が表示される

# 【11】temp の状態を表示(意図せず外部からアクセス可能)
print("temp(グローバル):", temp)  # 【11】→ 6 が表示される
実行例
合計: 12
temp(グローバル): 6

安全な設計例

def calculate_total_fixed(items):  # 【1】安全に合計を計算する構造
    total = 0  # 【2】合計の初期化
    for item in items:  # 【3】ループ処理開始
        temp = item * 2  # 【4】ループ内だけで使うローカル変数として定義(グローバルではない)
        total += temp  # 【5】そのまま加算
    return total  # 【6】最終的な合計値を返す

# 【7】安全な関数で再計算
result2 = calculate_total_fixed([1, 2, 3])  # 【7】同様に合計 12 を期待

# 【8】出力
print("安全な合計:", result2)  # 【8】

# 【9】temp がグローバルには存在しないことを確認(前の例が残っていなければ NameError)
try:
    print("temp(安全版):", temp)  # 【9】前例の temp が影響しないよう確認
except NameError as e:
    print("temp は定義されていません:", e)  # 【10】本来こうあるべき
実行結果
安全な合計: 12
temp は定義されていません: name 'temp' is not defined
実行結果
安全な合計: 12
temp は定義されていません: name 'temp' is not defined

本問のまとめ:設計的不整合の内容と回避策

【不整合の本質】
本来一時的に使われるべき変数(temp)がグローバルに存在してしまうことで、副作用がプログラム全体に波及する問題である。

【回避策:安全な設計指針】

設計ルール 理由
一時変数は常にローカルスコープで定義する グローバル空間の汚染を防止
global 宣言の使用は最小限に留め、目的を明確にする 状態変更の意図を明示し、可視性と責任範囲を限定する
ブロック内のみで使う変数名はローカル関数や内包表記で閉じる スコープ境界が明確になり、影響範囲を限定できる
可能であれば関数スコープの中でさらに with, for, if を使っても状態を分離する スコープ責任を明確に切り分けることができる

Q.19-4

Pythonにおけるスコープルールが関数再帰、クロージャ、ラムダ式などと結びつく場面を列挙し、それぞれが抱える設計上の課題を述べよ。

問題背景

【1】 Pythonのスコープルールと関数的構造の関係

PythonのスコープはLEGB(Local, Enclosing, Global, Built-in)ルールに従う。
このスコープ機構は特に次のような関数的な文法要素と深く結びついている:

構文 関係するスコープ 説明
関数再帰 Local / Global 関数名が自スコープに存在しないと再帰不能
クロージャ Enclosing 外側関数のローカル変数を内側関数が保持(nonlocal)
ラムダ式 Enclosing 外側のスコープに依存するが、代入不能(再束縛できない)

解答例とコードによる実践

【1】関数再帰とスコープの関係

再帰的探索関数をローカルで隠蔽した場合、再帰不能になる例

# 【1】ツリー構造の辞書データを定義
tree = {  # 【1】
    "a": {"b": {"c": {}}, "d": {}},  # 【2】
    "e": {}  # 【3】
}

# 【4】search_tree関数を定義。ツリー中に target が存在するかを探索する
def search_tree(data, target):  # 【4】
    # 【5】再帰的に呼び出す内部関数を定義する
    def search(node):  # 【5】

        search = "not a function anymore"  # 【6】← 関数名と同じ変数名を上書きしてしまっている(バグの原因)

        for key, child in node.items():  # 【7】ノードの各キーに対してループ処理
            if key == target:  # 【8】対象と一致するか判定
                return True  # 【9】見つかったら True を返す
            if isinstance(child, dict):  # 【10】子要素が辞書なら
                if search(child):  # 【11】← search が文字列になっているため TypeError になる
                    return True  # 【12】再帰的に子ノード内を探索して一致したら True
        return False  # 【13】見つからなければ False を返す

    return search(data)  # 【14】再帰探索を開始する
実行結果
TypeError: 'str' object is not callable

原因:search = "not a function anymore" によって、再帰関数 search が文字列に上書きされており、search(child) を呼び出したときに "not a function anymore"(...) となって TypeError が発生する。("c""a" → "b" → "c" の順に存在しているので True が返ることを期待)

# 【1】search_tree関数を定義。ツリー構造から target を探索する
def search_tree(data, target):  # 【1】
    # 【2】内部で再帰的に探索を行う関数を定義
    def search(node):  # 【2】
        for key, child in node.items():  # 【3】辞書の各キーと値に対してループ
            if key == target:  # 【4】もしキーが target と一致すれば
                return True  # 【5】True を返す(見つかった)
            if isinstance(child, dict):  # 【6】子要素が辞書であればさらに探索
                if search(child):  # 【7】再帰的に探索(ここでエラーは起きない)
                    return True  # 【8】内部でも見つかれば True を返す
        return False  # 【9】最終的に見つからなければ False を返す

    return search(data)  # 【10】search を呼び出して探索開始

# 【11】テスト用の辞書データ(ツリー構造)
tree = {
    "a": {"b": {"c": {}}, "d": {}},  # 【12】
    "e": {}  # 【13】
}

print(search_tree(tree, "c"))  # 【14】→ True(見つかる)
print(search_tree(tree, "z"))  # 【15】→ False(見つからない)
実行結果
True
False

【2】クロージャと非線形状態の管理

複数レイヤーの関数で nonlocal を意識しないと、状態が保持できない例

# 【1】カウンター機能を持つクロージャを返す関数
def multi_layer_counter():
    count = 0  # 【2】外側関数で定義された変数(クロージャで保持されるはず)

    def middle_layer():  # 【3】中間レイヤーの関数
        def inner_layer():  # 【4】実際にカウントを操作する内側の関数
            # nonlocal count ← 【注】この行がないため count はローカル変数と誤認される
            count += 1  # 【5】UnboundLocalError が発生(nonlocal が必要)
            return count  # 【6】更新後のカウントを返す
        return inner_layer  # 【7】inner_layer 関数を返す

    return middle_layer()  # 【8】middle_layer を呼び出して inner_layer を返す
実行結果
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

原因:count += 1 によって、Pythonは countinner_layer のローカル変数とみなすが、初期化されていないため UnboundLocalError が発生する。

# 【1】カウントを記憶するクロージャ関数を定義
def multi_layer_counter():  # 【1】
    count = 0  # 【2】外側スコープに変数 count を用意

    def middle_layer():  # 【3】さらにその内側の関数を定義
        def inner_layer():  # 【4】実際にカウント操作を行う関数
            nonlocal count  # 【5】count は外側の変数だと明示する
            count += 1  # 【6】カウントを1増やす
            return count  # 【7】現在のカウントを返す
        return inner_layer  # 【8】関数を返す

    return middle_layer()  # 【9】middle_layer を呼び出して inner_layer を返す

# 【10】カウンターを取得
counter = multi_layer_counter()

print(counter())  # 【11】→ 1
print(counter())  # 【12】→ 2
実行結果
1
2

【3】ラムダ式とスコープの遅延評価 ― 難しめの設計課題例

ループとラムダ式を組み合わせたリスト内包表記における評価タイミングの問題

# 【1】ラムダ式を含むリスト内包で、ループ変数の評価が遅延される例
generators = [(lambda: i ** 2) for i in range(5)]  # 【1】

# 【2】すべてのラムダはループ終了後の i=4 を参照
results = [f() for f in generators]  # 【2】

print(results)  # 【3】→ [16, 16, 16, 16, 16]

原因:リスト内包時点で lambdai を束縛せず、実行時にグローバルの i を参照する。よって range(5) の最後の値(i=4)がすべてのラムダで共有されている。

# 【1】ループ変数 i を lambda のデフォルト引数で束縛して記憶
generators = [(lambda i=i: i ** 2) for i in range(5)]  # 【1】

# 【2】各ラムダを呼び出して結果をリストに格納
results = [f() for f in generators]  # 【2】

print(results)  # 【3】→ [0, 1, 4, 9, 16]
実行結果
[0, 1, 4, 9, 16]

本問のまとめ

構文種別 設計課題の本質 対応策
再帰関数 名前の再束縛によって関数オブジェクトが失われる 名前の衝突を避ける。内部関数は別名を使う
クロージャ nonlocal を宣言しないと変数の再代入が失敗する 状態更新が必要な場合は必ず nonlocal を使う
ラムダ式 ループ変数の評価が遅延され、すべて同じ変数を参照する lambda i=i: ... のようにデフォルト引数で束縛する

あとがき

今回は「グローバル変数とローカル変数」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回は、「引数の既定値」について扱う予定です。

参考文献

[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?