まえがき
今回からしばらく、Python3.13環境を想定した演習問題を扱います。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
文法書について初学者の方には、特に[1]がお勧めです。
・扱っている内容の過不足のなさ(初学者でもこれは知っておくべきということは大抵書かれていると感じる)が、他の本と比較して圧倒的に優れていること。
・文法の存在の必然性や、このようにコーディングしなければらなない理由(他にも書き方はあるが、なぜこの書き方をすべきなのか?)が明確に記載されていること。
・アルゴリズムについての言及があること(特にスクールや独学で学んだ人は、アルゴリズムに対する意識が向きづらい傾向にあると思います)。
の3点が主な理由です。GW空けには第2版も出るそうですね(Python3.13対応の文法書は少ないので、私は買います!)
2000問ほどあるストックを問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
今回は変数について扱います。それでは始めましょう!
Q.1-1
x, y = y, x
のような同時代入構文を用いた変数の交換処理の正当性を論ぜよ。
問題背景:tupleのunpack(unpacking assignment)と多重代入
Pythonにおいて、複数の変数に対して同時に値を代入する構文がサポートされています(多重代入/sequence unpacking)。
x, y = y, x
上記であれば、右辺のy,x
がtupleとして評価され(つまり(y, x)
という1つのtupleが生成され)、その後、左辺の(x, y)
に同時に展開(unpack)されるということです。
Pythonにおいては、右辺→左辺の順に評価が行われますが、右辺全体は最初に評価されたうえでtupleとして一時保存されるため、変数の値の「入れ替え」を破壊せずに行うことができます。
これがCやJavaなら、変数の値の交換ならば、一時変数(temp)を用いる必要があるのですが、Pythonならば一時変数は不要です(tuple生成とunpack代入のおかげ)。この構文は、可読性に優れ、安全かつエラーも起こりにくいので、Pythonicであるとされています。
解答例(コメントアウト付き)
# 【1】変数 x に整数 10 を代入する
x = 10
# 【2】変数 y に整数 20 を代入する
y = 20
# 【3】値の交換前の状態を表示
print(f"交換前: x = {x}, y = {y}")
# 【4】x, y = y, x によって、x と y の値を入れ替える
# このとき、右辺 (y, x) がまずタプルとして評価される → (20, 10)
# その後、それぞれ左辺にアンパックされる → x = 20, y = 10
x, y = y, x
# 【5】値の交換後の状態を表示して、正しく交換できたか確認する
print(f"交換後: x = {x}, y = {y}")
補足:内部的な動作(疑似的な展開)
x, y = y, x
は、Pythonインタプリタにとって、以下のように内部的に扱われていると考えてよい。
_temp = (y, x) # tupleの作成(右辺評価)
x = _temp[0]
y = _temp[1]
このように、右辺は評価順序が保証されているため、変数の上書きによって、他方の値が失われることがないため、信頼できる構文ということが出来ます。また、必要に応じて、変数がもっと多くても同様に利用可能です。例えば、
a, b, c = c, a, b
Q.1-2
x = y = [1, 2]
に対し、y.append(3)
を行った後の x
の内容が変化する理由を id()
とともに示せ。
問題背景:オブジェクトの参照と可変オブジェクト
Pythonでは、変数は「オブジェクトのラベル」に過ぎず、値そのものではありません(cf.) 値渡しと参照渡し:今後記事で扱います)。したがって、x = y = [1, 2]
と書くと、リスト[1, 2]
という1つのオブジェクトが作成され、それに対してx
とy
が同時に参照されます。
ここで、y.append(3)
を実行すると、そのオブジェクト自体が変更される。x
も同じオブジェクトを参照しているため、x
の「見かけ上の内容」も変わったように見えるというわけです。
ここで注意すべき文法として、
・Pythonの=
は「値のコピー」ではなく、「オブジェクトへの参照(ラベル付け)」を意味する
・list
, dict
, set
などは 可変(mutable)オブジェクトである
・同じオブジェクトに複数の変数がバインドされている場合、一方の変数で内容を変更すると、他方から見ても変更されている
・id()
関数を使うと、変数が指している 実体のメモリID(≒オブジェクト識別子)を調べられる
解答例(コメントアウト付き)
# 【1】変数 x と y に同時にリスト [1, 2] を代入(=同じオブジェクトを指すようにする)
x = y = [1, 2]
# 【2】x と y の内容を表示(ともに [1, 2] である)
print(f"xの内容(変更前): {x}")
print(f"yの内容(変更前): {y}")
# 【3】x と y が参照するオブジェクトのIDを表示(同じIDになる)
print(f"xのid: {id(x)}")
print(f"yのid: {id(y)}")
# 【4】y に対して append を実行し、リストに 3 を追加する
y.append(3)
# 【5】x の内容を再度表示(y に対する操作が x にも反映されていることを確認)
print(f"xの内容(変更後): {x}")
print(f"yの内容(変更後): {y}")
# 【6】ID を再度確認(x と y の id は依然として同じ → 同一オブジェクト)
print(f"xのid(変更後): {id(x)}")
print(f"yのid(変更後): {id(y)}")
論述問題の解答としては、
「x = y = [1, 2]
によって、xとyは同一のリストオブジェクトを指す。しかし、.append()
メソッドは破壊的であるから、y.append(3)
によって、xを通してみたときにも変更が反映される。id()
で確認すると、変更前後ともにx, yのidが一致していることが確認できます。
ということを、上記のコードを示しながら述べればよいでしょう。
補足:値のcopyが必要な場合
変更が波及しないようにしたい場合は、listのcopyを行います。例えば、
x = [1, 2]
y = x.copy # or y = list(x)
のように書けば、xとyは異なるオブジェクトを指すことになります。
Q.1-3
変数宣言時にis
によって参照の同一性を確認することが必要となる場面について述べよ。
問題背景
ここでは、特に==
演算子とis
演算子の違いについて特に扱う。簡単に書けば以下のような形になる。
比較方法 | 用途 |
---|---|
== (等値比較) |
値が等しいかどうかを判定(内容の比較) |
is (同一性比較) |
同じオブジェクト(ID)を参照しているかを判定 |
即ち、Pythonのis
演算子は、2つの変数が同じメモリ上のオブジェクトを指しているかを判定します。これは、「参照の同一性」と呼ばれ、パファーマンス上・正当性上の確認が重要な場面で登場する。is
演算子は以下のような場面で必要性が特に生じることに注意して下さい。
-
None
との比較
→ Pythonでは、値が未設定かどうかの判定には、is None
を用いるのが基本(== None
は非推奨)。 - シングルトンオブジェクト(
TRUE
,None
など)との比較
→ これらのオブジェクトは、Python内部でただ1つしか存在しないため、is
演算子による同一性比較が正しい - キャッシュされたimmutable objectの挙動確認(
int
,str
など) - 大規模オブジェクトや共有オブジェクトを操作する際に、同一性を確認する必要があるとき
解答例(コメントアウト付き)
# 【1】変数 a に None を代入(未設定の状態を表す)
a = None
# 【2】変数 a が None かどうかを is で確認(None はシングルトン:is を使うべき)
if a is None:
print("a は None(未設定の状態)です") # 出力される
else:
print("a は何か別の値を持っています")
# 【3】変数 b, c に同じ小さな整数(イミュータブル)を代入(キャッシュ対象)
b = 100
c = 100
# 【4】b と c の ID(実体)を確認(同じになる可能性が高い)
print(f"b の ID: {id(b)}")
print(f"c の ID: {id(c)}")
print(f"b is c → {b is c}") # True(CPythonではキャッシュ)
# 【5】次にリストのような可変オブジェクトを比較
list1 = [1, 2, 3]
list2 = [1, 2, 3]
# 【6】値の比較(==)は True になる
print(f"list1 == list2 → {list1 == list2}") # True
# 【7】オブジェクトの同一性比較(is)は False(別オブジェクト)
print(f"list1 is list2 → {list1 is list2}") # False
# 【8】list1 に list2 を代入(同じオブジェクトを参照させる)
list2 = list1
# 【9】再度同一性比較(is)は True(同じものを指している)
print(f"list1 is list2(再代入後) → {list1 is list2}") # True
論述としての解答は、上記1.~4.をまとめて説明し、例としてコーディングを示せばよいでしょう。
Q.1-4
Pythonにおいて、関数内で変数を宣言する際に global およびlocal が必要となるケースと不要なケースを比較し、その理由を論ぜよ。
問題背景
このあたりは、表にしてまとめてしまうのが分かりやすいと思います。
コープと変数束縛の規則(LEGBルール)
Python の変数スコープは次の4層に従って解決されます(LEGBルール):
スコープ | 説明 |
---|---|
Local | 現在の関数内で定義された変数 |
Enclosing | 外側の関数(ネストされた関数)の変数 |
Global | モジュールレベルの変数 |
Built-in | Python が元から持っている組込み名前 |
また、globalやnonlocalが必要/不必要なケースはそれぞれ以下です。
ケース | 必要性 | 理由 |
---|---|---|
関数内でグローバル変数に代入する | 必要 | Python は代入文を見て「ローカル変数」と解釈するため、global で「これは外の変数」と明示する必要がある |
関数内でグローバル変数を参照のみ | 不要 | グローバルスコープから自動で探索されるため global は不要 |
ケース | 必要性 | 理由 |
---|---|---|
ネスト関数内で外側のローカル変数に代入したい | 必要 | Python は代入文をローカル変数とみなすため、nonlocal で「外側スコープ」と明示する必要がある |
ネスト関数内で外側の変数を参照のみ | 不要 | 参照は enclosing スコープから自動で探索されるため |
これをスコープと挙動という観点から比較すると、以下の通りです。
比較項目 | global |
nonlocal |
---|---|---|
操作対象スコープ | グローバルスコープ(モジュール全体) | 一つ外側の関数スコープ(ローカル) |
宣言位置 | 関数内 | ネスト関数内(関数の中の関数) |
参照対象がないと? | 新たにグローバル変数を作る | エラー(外側の変数がなければ例外) |
用途例 | 状態共有、設定値変更など | クロージャ変数の更新 |
上記を理解できれば、以下のコードで解答は掃けていると感じるでしょう。
解答例(コメントアウト付き)
# 【1】グローバル変数を定義(global を使う対象)
count = 0
# 【2】global を使う例:関数内からグローバル変数に代入
def increment_global():
global count # 【3】count はグローバル変数と明示
count += 1 # 【4】グローバル変数を更新
# 【5】nonlocal を使う例:ネスト関数内から enclosing スコープの変数を更新
def outer():
message = "hello" # 【6】この変数を内側関数から変更したい
def inner():
nonlocal message # 【7】message は外側スコープの変数であると明示
message = "world" # 【8】nonlocal がなければローカルとみなされ、元の message は変更されない
inner() # 【9】内側の関数を呼び出す
print(f"[outer] message = {message}") # 【10】変更が反映されていることを確認
# 【11】increment_global を複数回呼び出す
increment_global()
increment_global()
# 【12】結果を出力(count: 2)
print(f"[main] count = {count}")
# 【13】outer を呼び出して nonlocal の挙動を確認
outer()
# 出力結果
[main] count = 2
[outer] message = world
補足
今一度、global
とnonlocal
の必要性を表にまとめましょう。
目的 |
global が必要 |
nonlocal が必要 |
---|---|---|
グローバル変数への代入 | 必要 | 不要 |
外側関数のローカル変数への代入 | 不要 | 必要 |
グローバル変数の参照 | 不要 | 不要 |
ネスト関数外の変数の参照 | 不要 | 不要 |
ただし上記において、
・ global
やnonlocal
の多用は、コードの可読性や保守性を損なう恐れがあること
・ 状態管理は、可能な限り引数と戻り値で明示的に行うことが推奨される
ことに注意してください。
あとがき
今回は変数について扱いました。「変数」の扱う範囲は広いので、もしかすると何問か追加するかもしれません。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)