Python講座 / 変数のスコープ・別名・可変オブジェクトを理解する
スコープは名前をどこで探すかを決め、可変性は共有するオブジェクトの変更がどこから見えるかを決めます。代入と変更を分けると、関数の呼び出し後に元のデータまで変わる理由を説明できます。
ローカルな名前と外側の名前を分ける
関数内で代入する名前は、特別な宣言がなければ通常はローカルです。外側の値を読むことと、外側の名前へ再代入することは別です。必要な値を引数で受け、結果をreturnすると、グローバルな状態へ依存するよりテストしやすくなります。
globalはモジュールの束縛、nonlocalは外側の関数の束縛を参照します。宣言を追加する前に、引数と戻り値でデータの流れを明確にできないかを検討します。
関数のデフォルト値と共有状態は、インスタンス属性とクラス属性で扱う規則とつながります。
共有リストとコピーを比較する
scope.pyへ全体コードを保存し、仮想環境でpython scope.pyを実行します。
def add_fee(items, fee):
items.append(fee)
original = [3500, 8000]
alias = original
copy = original.copy()
add_fee(alias, 500)
print(original)
print(copy)
nested = [[1], [2]]
shallow = nested.copy()
shallow[0].append(9)
print(nested)
予想結果です。外側のコピーでは金額が増えませんが、入れ子の内側リストは共有されています。
[3500, 8000, 500] [3500, 8000] [[1, 9], [2]]
代入・浅いコピー・深いコピーを読む
alias = originalは同じリストへ別名を付けます。copy()は外側のリストを新しく作りますが、内側の要素への参照は共有します。そのためshallow[0]を変更すると、nestedから見える内側のリストにも9が加わります。
copy.deepcopyは構造を再帰的にコピーする道具ですが、ファイル、接続、意図的に共有した状態などを何でも適切に複製する方法ではありません。独立させたいデータだけを組み直すか、要件に合うなら不変な値を使います。
関数が入力を変えるかを契約にする
呼び出し側が、渡したリストを変更する関数かどうかを判断できることが重要です。入力を保つ関数なら、新しいリストを返す方法もあります。戻り値だけでなく元の入力もテストすると、予想外の副作用を見つけられます。
add_fee(copy, 100)ではoriginalへ100が増えないことを確認します。関数内のappendをitems = items + [fee]へ変えると、外側のリストが変わらない理由を考え、元へ戻してください。外側の独立性と入れ子の共有は別々に説明します。既定引数にも、呼び出しをまたいで可変オブジェクトが共有される例があります。
通常の関数内の名前はLocal(ローカル)、Enclosing(外側の関数)、Global(モジュール)、Builtins(組み込み)の順に探します。global・nonlocal宣言がなければ、関数内で代入する名前はローカルとなり、代入前に読むとUnboundLocalErrorです。再代入は名前の参照先を変え、appendは参照先のリストを変更します。次の例ではモジュールの10は維持され、nonlocalが外側のカウンターを更新します。
全体を scope.py に保存し、同じ練習フォルダーで python scope.py を実行します。
value = 10
def local_read():
value = 20
return value
def broken():
value += 1
return value
print(local_read(), value)
try:
broken()
except UnboundLocalError:
print("local name not initialized")
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter())
予想結果:
20 10 local name not initialized 1 2
動作と適用条件の公式説明:Python: resolution of names




