2.

Python の変数|代入の仕組み・スコープ・global と nonlocal の使い方

編集
この記事の要点
  • Python の変数に型宣言は要らないx = 1 と書いた時点で使える
  • 代入は「箱に入れる」ではなくオブジェクトに名前を付ける操作。b = a は同じ実体を 2 つの名前で指す
  • 関数の中で代入した名前はローカル変数。外の変数を書き換えるには global / nonlocal が要る
  • デフォルト引数にリストや辞書を書くと使い回されるNone を既定にして関数内で作る
  • 定数は言語機能として存在しない。MAX_RETRY のように大文字で書く約束だけ

変数の作り方

count = 10           # 宣言と代入が同時。型は書かない
name = "田中"
price = 1980.5
is_active = True

count = "十"         # 別の型を入れ直しても構わない(推奨はしない)

# まとめて代入
x, y = 1, 2
x, y = y, x          # 入れ替え。一時変数は要らない
a = b = c = 0        # 同じ値を 3 つの名前に

# 使い捨ての値は慣習として _ を使う
for _ in range(3):
    print("hello")

定義していない名前を読むと NameError: name 'foo' is not defined になります。値が未定であることを表したいときは foo = None と明示的に入れておきます。

代入は「名前を付ける」操作

ここが他の言語と最も感覚が違う部分です。Python の変数は値そのものを持たず、オブジェクトへの参照を持ちます。

a = [1, 2, 3]
b = a                # 同じリストに b という名前も付けただけ
b.append(4)
print(a)             # [1, 2, 3, 4]   ← a も変わって見える
print(a is b)        # True           同一のオブジェクト

# コピーしたいなら明示する
c = a.copy()         # または list(a) / a[:]
c.append(5)
print(a)             # [1, 2, 3, 4]   影響なし

整数や文字列では同じことが起きません。これらはイミュータブル(変更できない)で、n += 1 は新しい整数を作って名前を付け替えているだけだからです。

分類共有しても安全か
イミュータブルint float str tuple frozenset bool None安全
ミュータブルlist dict set 自作クラスの多く片方の変更が両方に見える

ネストしたコピーには注意

import copy

matrix = [[1, 2], [3, 4]]
shallow = matrix.copy()      # 浅いコピー。中のリストは共有されたまま
shallow[0].append(99)
print(matrix)                # [[1, 2, 99], [3, 4]]  ← 影響が出る

deep = copy.deepcopy(matrix) # 深いコピー
deep[0].append(0)
print(matrix)                # [[1, 2, 99], [3, 4]]  影響なし

スコープ(変数が見える範囲)

total = 0                # モジュール(グローバル)変数

def show():
    print(total)         # 読むだけなら外の変数が見える

def broken():
    print(total)         # UnboundLocalError
    total = 1            # ← この代入があるため total は「ローカル変数」扱いになる

def fixed():
    global total         # 外の total を書き換えると宣言する
    total = 1

def outer():
    n = 0
    def inner():
        nonlocal n       # 1 つ外側の関数の n を書き換える
        n += 1
    inner(); inner()
    return n             # 2

関数の中で 1 度でも代入した名前は、その関数のローカル変数になります。関数の先頭で参照していても同じです。これが UnboundLocalError の正体で、初心者がつまずく最頻出ポイントです。

ただし global は多用すると「どこで書き換わったか」が追えなくなります。値を返して呼び出し側で受け取るのが基本形です。

ループ変数と if 文はスコープを作らない

for i in range(3):
    last = i
print(i, last)       # 2 2   ループを抜けても残っている

if True:
    inside = "見える"
print(inside)        # 見える

# 内包表記だけは別スコープなので漏れない
squares = [n * n for n in range(3)]
print(squares)       # [0, 1, 4]
# print(n)           # NameError

スコープを作るのは関数・クラス・モジュールだけです。ブロックごとに変数が消える言語から来ると意外に感じる部分です。

デフォルト引数の落とし穴

def add_bad(item, bucket=[]):     # 危険
    bucket.append(item)
    return bucket

print(add_bad("a"))     # ['a']
print(add_bad("b"))     # ['a', 'b']   ← 前回の内容が残る

def add_ok(item, bucket=None):    # 正しい書き方
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(add_ok("a"))      # ['a']
print(add_ok("b"))      # ['b']

デフォルト値は関数を定義したときに 1 回だけ評価されます。ミュータブルな値を既定にすると、その 1 個を全呼び出しで共有してしまいます。

定数と型ヒント

from typing import Final

MAX_RETRY = 3                  # 大文字=定数のつもり、という約束のみ
TIMEOUT: Final = 30            # 再代入を mypy などが警告してくれる

count: int = 0
label: str | None = None       # None もあり得る

MAX_RETRY = 5                  # 実行はできてしまう(Python は止めない)

Python に const はありません。Final を付けても実行時には効かず、静的解析ツールが警告するだけです。

アンパック代入

point = (1, 2)
x, y = point                 # タプルを展開して受け取る

first, *rest = [1, 2, 3, 4]  # first=1, rest=[2, 3, 4]
*head, last = [1, 2, 3, 4]   # head=[1, 2, 3], last=4
a, _, c = (1, 2, 3)          # 要らない値は _ で受ける

for name, age in [("田中", 30), ("鈴木", 25)]:
    print(name, age)

d = {"a": 1, "b": 2}
for key, value in d.items():
    print(key, value)

x, y = 1, 2, 3               # ValueError: too many values to unpack (expected 2)

個数が合わないと ValueError になります。アスタリスクを付けた変数は「残り全部」を受け取り、必ずリストになります。

変数の中身を調べる

value = [1, 2, 3]

print(value)                 # [1, 2, 3]
print(repr(value))           # [1, 2, 3]   型が紛れないよう調査時はこちら
print(type(value))           # <class 'list'>
print(id(value))             # 140234...   オブジェクトの識別子

# f-string の = 記法(Python 3.8 以降)は変数名も一緒に出せる
count = 10
print(f"{count=}")           # count=10

# いま見えている名前の一覧
print(sorted(locals().keys()))
print("count" in globals())  # True

f"{count=}"変数名と値をまとめて出力します。print("count:", count) と書く手間が省け、名前を書き間違えることもありません。

使い終わった変数を消す

big = list(range(10_000_000))
del big                      # 名前を消す。参照が 0 になれば実体も解放される
# print(big)                 # NameError

a = [1, 2, 3]
b = a
del a                        # b がまだ指しているので実体は残る
print(b)                     # [1, 2, 3]

del が消すのは名前だけです。ほかの名前や、リスト・辞書の中から参照されていれば、メモリ上の実体は残り続けます。

環境変数と設定値

import os

# 無ければ None(KeyError にならない)
api_key = os.environ.get("API_KEY")

# 既定値を用意する
port = int(os.environ.get("PORT", "8000"))

# 必須なら早い段階で落とす
db_url = os.environ["DATABASE_URL"]     # 無ければ KeyError で起動時に気づける

環境変数の値は必ず文字列です。数値として使うなら明示的に変換します。パスワードや API キーをソースコードに直接書かないでください。

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. 変数
  3. 演算子
  4. 標準ライブラリ
  5. 外部ライブラリ
  6. 制御構文
  7. リスト(配列)
  8. タプル
  9. セット
  10. 辞書(dict)
  11. クラスとメソッド
  12. 継承の概念と必要性
  13. 継承の構文
  14. コンストラクタ
  15. cookieの値の設定と取得
  16. 例外処理
  17. 例外を文字列で出力する方法
  18. httpリクエスト(curl)をする方法
  19. Responseオブジェクトの中身の確認
  20. 変数が空かどうか判定する方法
  21. タイムゾーンの設定と現在日時の取得と文字列化
  22. シングルクォーテーションとダブルクォーテーションの違い

最近更新/作成されたページ