21. 変数スコープと名前解決
Python で変数を作るとき、それはどこに「存在」するのでしょうか?関数は、その外側で作られた変数を見ることができるのでしょうか?関数の外側のコードは、関数の内側で作られた変数にアクセスできるのでしょうか?これらの疑問は スコープ(scope)、つまり「名前が見えて使える」プログラム領域に関するものです。
スコープを理解することは、正しく予測可能に動く関数(function)を書くために重要です。この知識がないと、変数が期待どおりの値にならない、あるいは変数の変更が意図したとおりに維持されない、といったバグをうっかり作ってしまうことがあります。
この章では、Python が名前がどの変数を参照しているのかをどのように判断するのか、変数にアクセスできる範囲をどう制御するのか、そして名前を削除したときに何が起きるのかを見ていきます。読み終えるころには、Python プログラムにおける変数の可視性を支配するルールが理解できるようになります。
21.1) ローカル変数とグローバル変数
Python のすべての変数は、特定の スコープ(scope)、つまりその変数名が定義されアクセスできるコード領域の中に存在します。もっとも基本的なスコープは ローカル(local) と グローバル(global) の 2 つです。
グローバルスコープの理解
プログラムのトップレベル、つまりどの関数の外側でも作られた変数は グローバルスコープ に存在します。これらは グローバル変数 と呼ばれ、定義された後はモジュール内のどこからでもアクセスできます。
# グローバル変数 - モジュールレベルで定義
total_users = 0
def show_user_count():
# この関数はグローバル変数を読み取りできる
print(f"Total users: {total_users}")
show_user_count() # Output: Total users: 0
print(total_users) # Output: 0この例では、total_users はグローバル変数です。関数 show_user_count() とモジュールレベルのコードの両方からアクセスできます。グローバル変数は、プログラムファイル全体で見えるものだと考えてください。
ローカルスコープの理解
関数の内側で作られた変数は、その関数の ローカルスコープ に存在します。これらは ローカル変数 と呼ばれ、定義された関数の中でしかアクセスできません。関数の実行が終わると、ローカル変数は消えます。
def calculate_discount(price):
# discount_rate はこの関数にローカルな変数
discount_rate = 0.15
discount_amount = price * discount_rate
return discount_amount
result = calculate_discount(100)
print(result) # Output: 15.0
# これはエラーになります - ここには discount_rate が存在しません
# print(discount_rate) # NameError: name 'discount_rate' is not defined変数 discount_rate と discount_amount は、calculate_discount() が実行されている間だけ存在します。関数が return した後、これらの名前はもう存在しません。これは実は良いことです。関数が一時的な変数でプログラムを散らかしてしまうのを防げるからです。
ローカルスコープが重要な理由
ローカルスコープは カプセル化(encapsulation) を提供します。つまり、各関数が自分専用の作業領域を持てます。これにより、異なる関数で同じ変数名を使っても衝突しません。
def calculate_tax(amount):
rate = 0.08 # ローカル変数
return amount * rate
def calculate_shipping(weight):
rate = 5.00 # 同じ名前だが別のローカル変数
return weight * rate
tax = calculate_tax(100)
shipping = calculate_shipping(3)
print(f"Tax: ${tax}") # Output: Tax: $8.0
print(f"Shipping: ${shipping}") # Output: Shipping: $15.0どちらの関数も rate という変数名を使っていますが、別々のローカルスコープにあるため完全に独立しています。一方の関数で rate を変更しても、もう一方の関数の rate には影響しません。この分離により、関数はより信頼でき、理解しやすくなります。
関数からグローバル変数を読み取る
関数は特別な構文なしでグローバル変数を読み取れます。
# グローバル設定
max_login_attempts = 3
def check_login(password):
# グローバル変数を読み取る
if password == "secret123":
return "Login successful"
else:
return f"Invalid password. You have {max_login_attempts} attempts."
result = check_login("wrong")
print(result) # Output: Invalid password. You have 3 attempts.関数 check_login() は max_login_attempts がグローバル変数なので読み取れます。ただし、ここで理解しておくべき重要な制限があります。
代入はローカル変数を作るというルール
ここからスコープがややこしくなります。関数の中で変数名に代入すると、同名のグローバル変数が存在していても、Python はその名前で 新しいローカル変数 を作ります。
counter = 0 # Global variable
def increment_counter():
# WARNING: This creates a NEW local variable named counter - for demonstration only
# PROBLEM: Trying to read counter before assigning to it locally
counter = counter + 1 # UnboundLocalError: local variable 'counter' referenced before assignment
print(counter)
# increment_counter() # This call results in UnboundLocalErrorこのコードが失敗するのは、Python が counter = counter + 1 という代入を見て、counter はローカル変数だと判断するからです。しかし counter + 1 を評価しようとした時点では、ローカル変数 counter にはまだ値がなく、代入前に使おうとしてしまっています。
これはよくある混乱の原因です。ルールはこうです。関数が本体のどこかである変数名に代入するなら、その名前は関数全体を通してローカルとして扱われます。代入より前でも同様です。
よりはっきり見てみましょう。
message = "Hello" # Global variable
def show_message():
print(message) # This works - just reading the global
def change_message():
# WARNING: This demonstrates a common error - for demonstration only
# PROBLEM: Python sees assignment below, so message is treated as local throughout
print(message) # UnboundLocalError!
message = "Goodbye" # This makes message local for the ENTIRE function
show_message() # Output: Hello
# change_message() # This call results in UnboundLocalError関数 show_message() は message を読むだけなので問題なく動きます。しかし change_message() は、2 行目の代入によって Python が message を関数全体でローカル扱いしてしまい、代入より前の print() でもローカル変数として参照しようとして失敗します。
パラメータはローカル変数
関数のパラメータはローカル変数で、関数呼び出し時に渡された引数から初期値を受け取ります。
def greet(name): # 'name' はローカル変数
greeting = f"Hello, {name}!" # 'greeting' もローカル
return greeting
message = greet("Alice")
print(message) # Output: Hello, Alice!
# ここには 'name' も 'greeting' も存在しません
# print(name) # NameErrorパラメータ name は greet() の中でだけ存在します。関数が呼ばれたときに作られ、関数が return すると消えます。
実用例: ショッピングカートの計算
ローカルスコープとグローバルスコープが現実的な場面でどう協調するかを見てみましょう。
# グローバル設定
tax_rate = 0.08
free_shipping_threshold = 50
def calculate_total(subtotal):
# この計算のためのローカル変数
tax = subtotal * tax_rate # グローバル tax_rate を読み取る
# 送料を決定する
if subtotal >= free_shipping_threshold: # グローバルしきい値を読み取る
shipping = 0
else:
shipping = 5.99
total = subtotal + tax + shipping
return total
# 異なるカート金額で計算する
cart1 = calculate_total(30)
cart2 = calculate_total(60)
print(f"Cart 1 total: ${cart1:.2f}") # Output: Cart 1 total: $38.39
print(f"Cart 2 total: ${cart2:.2f}") # Output: Cart 2 total: $64.80この例では以下のとおりです。
tax_rateとfree_shipping_thresholdはグローバルな設定値ですsubtotal、tax、shipping、totalはcalculate_total()の各呼び出しにローカルです- 関数呼び出しごとに、それぞれ独立したローカル変数セットを持ちます
- 関数はグローバル設定を読み取れますが、変更はしません
この関心事の分離により、コードは明確になります。グローバル変数はどこでも適用される設定を持ち、ローカル変数は関数呼び出しごとに固有の一時的な計算結果を保持します。
21.2) 名前解決のための LEGB ルール
Python が変数名に出会ったとき、どの変数を指しているのかをどうやって知るのでしょうか?Python は LEGB ルール と呼ばれる特定の探索順序に従います。LEGB は Local, Enclosing, Global, Built-in の略で、Python がその順に探索する 4 つのスコープを表します。
LEGB における 4 つのスコープ
LEGB 階層の各スコープを理解しましょう。
- Local (L): 現在の関数のスコープ
- Enclosing (E): 外側の関数(現在の関数を含む関数)のスコープ
- Global (G): モジュールレベルのスコープ
- Built-in (B):
print、len、intなど、Python の組み込み名
変数名を使うとき、Python は L → E → G → B の順にスコープを探します。最初に見つかった一致を使い、探索を止めます。
ローカルスコープ: Python が最初に探す場所
Python は常に最初にローカルスコープをチェックします。
def calculate_price():
price = 100 # ローカル変数
tax = 0.08 # ローカル変数
total = price * (1 + tax)
return total
result = calculate_price()
print(result) # Output: 108.0Python が calculate_price() の中で price、tax、total を見ると、ローカルスコープで見つけてその値を使います。探索はローカルスコープで止まり、Python はそれ以上探す必要がありません。
グローバルスコープ: ローカルにない場合
名前がローカルで見つからない場合、Python はグローバルスコープをチェックします。
# グローバル変数
default_tax_rate = 0.08
default_currency = "USD"
def calculate_price(amount):
# 'amount' はローカルで即座に見つかる
# 'default_tax_rate' はローカルではなく、グローバルスコープで見つかる
total = amount * (1 + default_tax_rate)
return total
result = calculate_price(100)
print(result) # Output: 108.0Python が関数内で default_tax_rate に出会うと、ローカルでは見つからないためグローバルスコープを探索し、そこで見つけます。
組み込みスコープ: Python が事前に用意した名前
名前がローカルにもグローバルにも見つからない場合、Python は組み込みスコープをチェックします。これは Python が自動的に提供する名前です。
def process_data(numbers):
# 'numbers' はローカル
# 'len' はローカルでもグローバルでもない - 組み込み
count = len(numbers)
# 'max' も組み込み
maximum = max(numbers)
return count, maximum
data = [10, 25, 15, 30, 20]
result = process_data(data)
print(result) # Output: (5, 30)len と max はあなたのコードで定義していません。Python が提供する組み込み関数です。Python がローカルやグローバルでこれらの名前を見つけられないとき、組み込みスコープをチェックして見つけます。
エンクロージングスコープ: ネストした関数
エンクロージングスコープ(enclosing scope) は、関数の入れ子(別の関数の中に定義された関数)がある場合に重要になります。ここで LEGB の「E」が効いてきます。
def outer_function():
outer_var = "I'm from outer" # inner_function にとってのエンクロージングスコープ内
def inner_function():
inner_var = "I'm from inner" # inner_function にローカル
# inner_function は inner_var(ローカル)と outer_var(エンクロージング)の両方を見える
print(inner_var) # Output: I'm from inner
print(outer_var) # Output: I'm from outer
inner_function()
outer_function()inner_function() にとって、outer_function() のスコープは エンクロージングスコープ です。inner_function() が outer_var を参照するとき、Python は次の順に探します。
inner_function()のローカルスコープ - 見つからないouter_function()のエンクロージングスコープ - 見つかった!この値を使う
LEGB を実際に見る: シンプルな例
4 つのスコープすべてが一緒に動く、わかりやすい例を見てみましょう。
# Built-in: len (Python provides this)
# Global: multiplier
multiplier = 10
def outer(x):
# inner にとってのエンクロージングスコープ
y = 5
def inner(z):
# ローカルスコープ
# z はローカル (L)
# y はエンクロージングスコープ (E)
# multiplier はグローバルスコープ (G)
# len は組み込みスコープ (B)
result = len([z, y, multiplier]) # 4 つのスコープすべてを使う!
return z + y + multiplier
return inner(3)
answer = outer(100)
print(answer) # Output: 18Python が inner() の中で z + y + multiplier を評価するとき:
- L (Local):
z = 3を見つける - E (Enclosing):
outer()の中でy = 5を見つける - G (Global):
multiplier = 10を見つける - B (Built-in):
len関数を見つける
この例は、Python が名前を解決するために 4 つすべてのスコープをどう探索するかをはっきり示しています。
シャドーイング: 内側のスコープが外側の名前を隠すとき
同じ名前が複数のスコープに存在する場合、最も内側のスコープが「勝ちます」。これを シャドーイング(shadowing) と呼びます。
value = "global"
def outer():
value = "enclosing"
def inner():
value = "local"
print(value) # Which value?
inner()
print(value) # Which value?
outer()
print(value) # Which value?Output:
local
enclosing
global各 print() 文が異なる value を見るのは、Python が最初に一致したところで探索を止めるからです。
inner()の内側:valueをローカルで見つける → "local" を出力outer()の内側だがinner()の外側:outer()のスコープでvalueを見つける → "enclosing" を出力- モジュールレベル:
valueをグローバルで見つける → "global" を出力
LEGB の探索順序を可視化する
この図は Python の探索プロセスを示しています。最も内側のスコープから始め、外側へ向かって探索します。どのスコープでも名前が見つからなければ、Python は NameError を送出します。
関数を書く上で LEGB が重要な理由
LEGB を理解すると、次のことに役立ちます。
- 変数値を予測できる: Python がどの変数を使うかが正確にわかります
- 命名衝突を避けられる: 名前が互いにシャドーイングするときが理解できます
- より良い関数設計ができる: 各変数に適切なスコープを選べます
- スコープ問題をデバッグできる: 変数が期待どおりでないときに LEGB をたどれます
LEGB ルールは、Python が名前を解決する基本です。変数を使うたびに、Python は裏側でこのルールに従っています。
21.3) global キーワードを慎重に使う
関数はグローバル変数を読み取れることがわかりましたが、関数の中からグローバル変数を変更したい場合はどうでしょうか?そこで登場するのが global キーワードです。ただし、これは控えめに、慎重に使うべきです。
問題: 代入はローカル変数を作る
先ほど学んだように、関数の中で変数に代入するとローカル変数が作られます。
counter = 0 # Global variable
def increment():
# WARNING: This creates a NEW local variable named counter - for demonstration only
# PROBLEM: Trying to read counter before assigning to it locally
counter = counter + 1 # UnboundLocalError!
# increment() # This call results in UnboundLocalErrorこれは、Python が代入を見て counter を関数全体でローカル扱いするからです。しかしローカルに代入する前に counter を読もうとしてしまっています。
これはグローバル変数を扱うときの最も一般的なエラーの 1 つです。エラーメッセージ UnboundLocalError: local variable 'counter' referenced before assignment は、何が起きたかをそのまま伝えています。Python は(代入があるため)counter をローカルだと判断したのに、値を与える前に使ってしまったのです。
解決策: 変数をグローバルとして宣言する
global キーワードは Python にこう伝えます。「この名前で新しいローカル変数を作らないでください。代わりにグローバル変数を使ってください。」
counter = 0 # Global variable
def increment():
global counter # Python にグローバル counter を使うよう伝える
counter = counter + 1 # これでグローバル変数を変更する
print(f"Before: {counter}") # Output: Before: 0
increment()
print(f"After: {counter}") # Output: After: 1
increment()
print(f"After again: {counter}") # Output: After again: 2global counter の宣言は、その変数を使う前に書く必要があります。この関数内での counter への代入は、ローカル変数を作るのではなくグローバル変数を変更する、ということを Python に伝えます。
複数のグローバル変数
1 つの文で複数の変数をグローバルとして宣言できます。
total_sales = 0
total_customers = 0
def record_sale(amount):
global total_sales, total_customers
total_sales += amount
total_customers += 1
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $0, Customers: 0
record_sale(25.50)
record_sale(30.00)
print(f"Sales: ${total_sales}, Customers: {total_customers}")
# Output: Sales: $55.5, Customers: 2total_sales と total_customers の両方がグローバルとして宣言されているので、関数からどちらも変更できます。
global を使う場面: 共有状態
global キーワードが適切なのは 共有状態(shared state) を維持する必要があるときです。つまり、複数の関数がアクセスして変更する必要があるデータです。
# ゲーム状態
player_score = 0
player_lives = 3
game_over = False
def award_points(points):
global player_score
player_score += points
print(f"Score: {player_score}")
def lose_life():
global player_lives, game_over
player_lives -= 1
print(f"Lives remaining: {player_lives}")
if player_lives <= 0:
game_over = True
print("Game Over!")
def check_game_status():
# グローバルを読み取るだけ - global キーワードは不要
if game_over:
return "Game Over"
else:
return f"Playing - Score: {player_score}, Lives: {player_lives}"
# ゲームをプレイする
award_points(100) # Output: Score: 100
award_points(50) # Output: Score: 150
lose_life() # Output: Lives remaining: 2
print(check_game_status()) # Output: Playing - Score: 150, Lives: 2この例は global の適切な使用を示しています。複数の関数が共有のゲーム状態を変更する必要があります。ただし check_game_status() は変数を読むだけなので global は不要な点に注目してください。
global を慎重に使うべき理由
global は必要な場合もありますが、使いすぎるとコードが理解しづらく、保守もしにくくなります。理由は以下のとおりです。
問題 1: 依存関係が隠れる
関数がグローバル変数を変更すると、呼び出し側からは何が変わるのかが分かりにくくなります。
total = 0
def add_to_total(value):
global total
total += value
# この関数は何をする?コードを読まないと分からない
add_to_total(10)値を返す関数と比べてみましょう。
def add_to_total(current_total, value):
return current_total + value
total = 0
total = add_to_total(total, 10) # 明確: total が更新されている2 つ目のバージョンは、total が変更されることが明示的です。
問題 2: テストが難しくなる
グローバル状態を変更する関数は、テストのたびにグローバル変数の準備とリセットが必要になるため、テストが難しくなります。
# テストが難しい - グローバル状態に依存する
score = 0
def add_score(points):
global score
score += points
# 各テストで score をリセットする必要がある
# Test 1
score = 0
add_score(10)
assert score == 10
# Test 2 - もう一度 score をリセットしなければならない
score = 0
add_score(20)
assert score == 20問題 3: 関数が再利用できない
特定のグローバル変数に依存する関数は、他のプログラムで簡単に再利用できません。
# この関数は 'inventory' というグローバル変数がある場合にのみ動く
inventory = []
def add_item(item):
global inventory
inventory.append(item)global より良い代替案
多くの場合、戻り値とパラメータを使えば global を避けられます。
グローバル状態を変更する代わりに:
# global を使う(理想的ではない)
balance = 1000
def withdraw(amount):
global balance
if amount <= balance:
balance -= amount
return True
return False
withdraw(100)
print(balance) # Output: 900戻り値を使う:
# 戻り値を使う(より良い)
def withdraw(balance, amount):
if amount <= balance:
return balance - amount, True
return balance, False
balance = 1000
balance, success = withdraw(balance, 100)
print(balance) # Output: 9002 つ目のバージョンのほうが柔軟で、テストしやすく、再利用もしやすいです。
global が実際に適切な場合
global の正当な使い道もあります。
- 本当にグローバルである必要がある設定:
# アプリ全体の設定
debug_mode = False
log_level = "INFO"
def enable_debug():
global debug_mode, log_level
debug_mode = True
log_level = "DEBUG"- デバッグや統計のためのカウンタ:
# デバッグのために関数呼び出し回数を追跡する
_function_call_count = 0
def tracked_function():
global _function_call_count
_function_call_count += 1
# ... rest of functionglobal についての重要ポイント
globalはモジュールレベルの状態を本当に変更する必要があるときだけ使ってください- 代わりに戻り値とパラメータを使うことを優先してください
globalを使うときは、なぜ必要なのかを文書化してくださいglobalを避けられる設計に改善できないか検討してください- 覚えておいてください: グローバル変数の読み取りには
globalは不要で、変更するときだけ必要です
21.4) エンクロージング関数の変数を変更するために nonlocal を使う
ネストした関数では、外側(エンクロージング)の関数スコープの変数を変更したいことがあります。nonlocal キーワードはそのためのものです。global に似ていますが、グローバルスコープではなく、エンクロージング関数スコープに対して使います。
問題: エンクロージング変数の変更
代入がデフォルトでローカル変数を作るのと同様に、エンクロージングスコープでも同じ問題が起こります。
def outer():
count = 0 # outer のスコープにある変数
def inner():
# WARNING: This creates a NEW local variable named count - for demonstration only
# PROBLEM: Trying to read count before assigning to it locally
count = count + 1 # UnboundLocalError!
print(count)
inner()
# outer() # This call results in UnboundLocalErrorPython は inner() 内の count への代入を見て、count をローカル変数として扱います。しかしローカルに代入する前に読もうとしてしまい、エラーになります。
解決策: nonlocal キーワード
nonlocal キーワードは Python にこう伝えます。「この変数はローカルではありません。エンクロージング関数のスコープで探して、それを使ってください。」
def outer():
count = 0 # outer のスコープにある変数
def inner():
nonlocal count # outer のスコープの count を使う
count = count + 1
print(f"Count in inner: {count}")
print(f"Count before: {count}") # Output: Count before: 0
inner() # Output: Count in inner: 1
print(f"Count after: {count}") # Output: Count after: 1
outer()これで inner() は outer() のスコープにある count 変数を変更できます。エンクロージングスコープにある実体の変数を変更しているので、inner() が return した後も変更が維持されます。
nonlocal が便利な理由: 状態を覚える関数
nonlocal キーワードにより、内側の関数がエンクロージングスコープの状態を保持し、それを変更できる強力なパターンが可能になります。クロージャ(closure)やファクトリ関数(factory function)については第 23 章で詳しく学びますが、ここでは nonlocal が内側の関数にエンクロージングスコープの変数を変更させる、ということを理解してください。
以下は nonlocal の動作がわかる簡単な例です。
def create_counter():
count = 0 # increment にとってのエンクロージングスコープにある変数
def increment():
nonlocal count # エンクロージングスコープの count を変更する
count += 1
return count
return increment # 内側の関数を返す
# カウンタを作成する
counter1 = create_counter()
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
# もう 1 つ独立したカウンタを作成する
counter2 = create_counter()
print(counter2()) # Output: 1
print(counter2()) # Output: 2create_counter() を呼び出すたびに、新しい count 変数と、その count を nonlocal で変更できる新しい increment() 関数が作られます。
nonlocal と global の違い
違いを理解することが重要です。
x = "global"
def outer():
x = "enclosing"
def use_global():
global x # グローバルの x を参照する
print(f"use_global sees: {x}") # Output: use_global sees: global
def use_nonlocal():
nonlocal x # outer の x を参照する
print(f"use_nonlocal sees: {x}") # Output: use_nonlocal sees: enclosing
use_global()
use_nonlocal()
outer()globalは常にモジュールレベルのスコープを参照しますnonlocalは最も近いエンクロージング関数スコープを参照します
nonlocal を使えない場合
nonlocal キーワードは、エンクロージング関数スコープに対してのみ機能します。次の用途には使えません。
- グローバルスコープ(代わりに
globalを使います):
x = "global"
def func():
nonlocal x # SyntaxError: no binding for nonlocal 'x' found
x = "modified"- どのエンクロージングスコープにも存在しない変数:
def outer():
def inner():
nonlocal count # SyntaxError: no binding for nonlocal 'count' foundnonlocal についての重要ポイント
- エンクロージング関数スコープの変数を変更するには
nonlocalを使います nonlocalはグローバルスコープではなく、エンクロージング関数スコープを探索します- エンクロージング変数の読み取りには
nonlocalは不要で、変更するときだけ必要です nonlocalは、プライベートな状態を持つ関数を作るための強力なパターンを可能にします- クロージャとファクトリ関数については第 23 章でさらに学びます
nonlocal キーワードは、カウンタ、アカウント、イベントトラッカーの例で見たように、プライベートな状態を維持する関数を作るのに特に便利です。
21.5) del で名前(オブジェクトではない)を削除することと、その意味
ときには、長時間動作するプログラムでメモリを解放するため、一次的な変数を片付けるため、あるいはコレクションから要素を取り除くために、プログラムの名前空間から変数を取り除きたいことがあります。Python の del 文はこれらの作業を扱いますが、何をしていて何をしていないのかを正確に理解することが大切です。
Python の del 文はよく誤解されます。これはオブジェクトを削除するのではなく、名前(name)(変数の束縛)を削除します。この違いを理解することは、Python がメモリと参照をどのように管理しているかを理解する上で重要です。
del が実際にすること
del 文は現在のスコープから名前を取り除きます。
x = 42
print(x) # Output: 42
del x
# print(x) # NameError: name 'x' is not defineddel x の後、名前 x は現在のスコープに存在しなくなります。使おうとすると、名前がもう定義されていないため Python は NameError を送出します。
名前の削除 vs オブジェクトの削除
ここが重要なポイントです。del は名前を削除しますが、その名前が参照していたオブジェクトを必ず削除するわけではありません。
# リストを作り、それを参照する 2 つの名前を作る
original = [1, 2, 3]
reference = original # 両方の名前が同じリストを参照する
print(original) # Output: [1, 2, 3]
print(reference) # Output: [1, 2, 3]
# 片方の名前を削除する
del original
# 'reference' がまだ参照しているので、リストはまだ存在する
print(reference) # Output: [1, 2, 3]
# print(original) # NameError: name 'original' is not definedリスト [1, 2, 3] は reference がまだ参照しているため存在し続けます。original を削除したのは、その名前を取り除いただけで、リストオブジェクト自体を削除したわけではありません。
オブジェクトが実際に削除されるとき
Python は、どの名前からも参照されなくなったオブジェクトを自動的に削除します。これを ガベージコレクション(garbage collection) と呼びます。
data = [1, 2, 3] # リストが作られ、'data' がそれを参照する
del data # 'data' という名前が削除される
# これでリストには参照がないため、Python は最終的にそれを削除します
# (これは自動で行われます - 何もする必要はありません)data を削除すると、リスト [1, 2, 3] への参照が残らなくなるため、Python のガベージコレクタが最終的にメモリを回収します。ただしこれは自動で起こり、いつ実行されるかは制御できません。
コレクションから項目を削除する
del 文はコレクションから項目を削除することもできますが、これは名前を削除するのとは本質的に異なります。コレクションのインデックス指定やスライス指定で del を使う場合、名前を削除するのではなく、コレクション自体を変更しています。
ここは重要な区別です。del numbers[2] と書くとき、あなたはリストオブジェクトの特別なメソッドを呼び出して要素を取り除いています。名前 numbers は存在し続け、同じリストオブジェクトを参照したままです。ただし、リストの要素数が減ります。
# インデックス指定でリスト要素を削除する
numbers = [10, 20, 30, 40, 50]
del numbers[2] # インデックス 2 の要素を削除
print(numbers) # Output: [10, 20, 40, 50]
# リストのスライスを削除する
numbers = [10, 20, 30, 40, 50]
del numbers[1:3] # インデックス 1 から 3(除く)までの要素を削除
print(numbers) # Output: [10, 40, 50]
# 辞書エントリを削除する
person = {'name': 'Alice', 'age': 30, 'city': 'Boston'}
del person['age']
print(person) # Output: {'name': 'Alice', 'city': 'Boston'}