まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
2000問ほどあるストックを問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
今回は「代入演算子」について扱います。それでは始めましょう!
Q.3-1
x = 5; y = x += 1
のような構文が構文エラーになる理由と、適切な修正方法を記述せよ。
問題背景:代入式と加算代入(+=)
[1] 加算代入 +=
の特徴
・ +=
は、左辺の変数に右辺の値を加算して、結果を左辺の変数に代入する演算子である。
・ +=
は単一の演算子であり、演算と代入を一度に行うため、複数の代入を同時に行うことはできない。
・ 代入は右から順に評価されるが、x += 1
が左辺に代入されるため、Pythonは右辺でさらに評価を行うことが出来ない。
解答例(コメントアウト付き)
# 【1】x の初期値を設定
x = 5
# 【2】加算代入で x を 1 増やす(これが正しい構文)
x += 1 # 【3】x は 6 になる
# 【4】x の値を y に代入
y = x # 【5】y は 6 になる
# 【6】結果を確認
print(f"x = {x}, y = {y}") # → x = 6, y = 6
補足:+=
の代入式に関する注意
加算代入(+=
)は、演算子として左辺のオブジェクトを直接変更するため、新たに別のオブジェクトを作成することはない。もし、x
がlistやdictionaryのようなmutableなオブジェクトであれば、+=
演算子はそのオブジェクトの内容を直接変更する。
lst = [1, 2]
lst += [3] # lst は [1, 2, 3] となる
Q.3-2
x += 1
と x = x + 1
において、ミュータブル型とイミュータブル型で参照が変化する例を提示し、その挙動の違いを説明せよ。
問題背景:ミュータブル型 vs イミュータブル型
[1] ミュータブル型とイミュータブル型
Q.3-1の類題である。ここではミュータブル型とイミュータブル型を一度整理する。
分類 | 型の例 | 特徴 |
---|---|---|
イミュータブル型 | int, float, str, tuple | 値の変更ができず、新しいオブジェクトが生成される |
ミュータブル型 | list, dict, set, bytearray | 値をその場で変更でき、同じオブジェクトを保持する |
[2] x += 1
と x = x + 1
の違い(演算子の視点)
・ x += 1
は、in-place演算であり、__iadd__()
が呼ばれる(可能であれば元のオブジェクトを変更する)
・ x = x + 1
は「新しいオブジェクトの生成」であるから、__add__()
が呼ばれ、新しいオブジェクトが作成される。
解答例
イミュータブル型(例:int
)は、x += 1
でも x = x + 1
でも新しいオブジェクトを生成し、id()
は変化する。ミュータブル型(例:list
)では、x += [...]
は同じオブジェクトのままアドレス変更が行われ、id()
は変化しないことが多い。一方、x = x + [...]
は新しいオブジェクトが生成され、id()
は変更される。
# 【1】イミュータブル型:int の例
x = 10
print(f"[int] 初期 x の id: {id(x)}") # 【2】オリジナルの id
x += 1 # 【3】インプレース加算(__iadd__)→ 新しい int オブジェクトが生成される
print(f"[int] x += 1 の後の id: {id(x)}") # 【4】別のオブジェクトになる(id 変化)
x = x + 1 # 【5】通常の加算(__add__)→ 新しい int オブジェクトが生成される
print(f"[int] x = x + 1 の後の id: {id(x)}") # 【6】さらに別のオブジェクトになる
print()
# 【7】ミュータブル型:list の例
lst = [1, 2, 3]
print(f"[list] 初期 lst の id: {id(lst)}") # 【8】オリジナルの id
lst += [4] # 【9】インプレース加算(__iadd__)→ 元のリストが変更される
print(f"[list] lst += [4] の後の id: {id(lst)}") # 【10】同じ id(変更のみ)
lst = lst + [5] # 【11】通常の加算(__add__)→ 新しいリストが生成される
print(f"[list] lst = lst + [5] の後の id: {id(lst)}") # 【12】id が変化(別オブジェクト)
[int] 初期 x の id: 4373172352
[int] x += 1 の後の id: 4373172384
[int] x = x + 1 の後の id: 4373172416
[list] 初期 lst の id: 140465632035008
[list] lst += [4] の後の id: 140465632035008
[list] lst = lst + [5] の後の id: 140465632035200
Q.3-3
for i in range(10): print(i); i += 1 によって i の制御構造が変化しない理由を、イテレータの動作から説明せよ。
問題背景:for
loopとイテレーターの動作
[1] for
loopの内部構造
Pythonの for
文は、イテラブル(range
, list
, tuple
など)からイテレーターを取得し、毎回 next()
を呼び出して変数に値を代入する構文。これは以下のように動作をする。
it = iter(range(10)) # イテレータを生成
while True:
try:
i = next(it) # 次の要素を i に代入
print(i)
i += 1 # これはローカル変数 i を変更するだけ(イテレータには影響しない)
except StopIteration:
break
[2] i += 1
が意味を持たない理由
i += 1
は変数 i
に格納された値を変更しているのみ。しかし、for
loopの次回の反復では、再びイテレーターから新しい値が i
に代入されてしまうため、 i += 1
の変更は上書きされる。よって、i += 1
の変更はloopの制御構造に影響を及ぼさない。
解答例とコード
for i in range(10)
は、range(10)
に対してイテレータを生成し、次の i
に逐次代入を行う。この時、i += 1
によって i
の値を変更しても、loopの次の反復ではその変更が無視され、新しい値が i
に上書き代入される。そのため、イテレータは自己完結的に次の値を管理しており、loop変数 i
を通じては制御できない。
# 【1】for ループを使って 0〜9 を順に表示
for i in range(10):
print(f"現在の i = {i}") # 【2】i の値を表示(毎回 range から供給される)
i += 1 # 【3】この行は無意味(次回ループで上書きされる)
print(f"i += 1 後の i = {i}") # 【4】見た目は変わるが、次回には無効化される
# 出力例
現在の i = 0
i += 1 後の i = 1
現在の i = 1
i += 1 後の i = 2
現在の i = 2
i += 1 後の i = 3
...
現在の i = 9
i += 1 後の i = 10
→ 重要なのは「次のループでは i に range からの新しい値が入るため、i += 1 は無意味」ということ。
Q.3-4
x = 10; y = x; x += 1 によって y に影響しないことを確認し、参照と代入の関係を説明せよ。
問題背景:参照/代入とイミュータブル型の特性
[1] Pythonにおける変数の代入
Pythonの変数は、「値を格納する箱」ではなく、「オブジェクトの参照(ポインタ)」として振る舞う。
x = 10
y = x
このとき、x
と y
は 同じオブジェクト(10)を参照しているだけで、オブジェクト自体が複製されるわけではない。
[2] イミュータブル型の再代入と影響範囲
int
型は変更できないイミュータブル型である。x += 1
は、実際にはx = x + 1
と同じで、新しい整数オブジェクトを作り、それを x
に再代入していることになる。したがって、y
は依然もとの 10
を参照しているので影響を受けない。
解答例とコードによる実践
操作 y = x
により、y
は x
と同じ整数 10
を参照(参照のコピー)する。 x += 1
により、新たに整数 11
のオブジェクトが生成され、x
はそちらに参照を変更する。y
元のまま 10
を参照するため影響を受けない。
# 【1】整数オブジェクト 10 を x に代入
x = 10
# 【2】x の参照先(10)を y にも代入(同じオブジェクトを参照)
y = x
# 【3】x の id を確認(オブジェクトのアドレス)
print(f"[初期] x = {x}, id(x) = {id(x)}")
print(f"[初期] y = {y}, id(y) = {id(y)}")
# 【4】x に 1 を加算(新しいオブジェクト 11 を作り、それを x に代入)
x += 1
# 【5】x と y の値と id を確認
print(f"[変更後] x = {x}, id(x) = {id(x)}")
print(f"[変更後] y = {y}, id(y) = {id(y)}") # y は変わらず
# 出力例
[初期] x = 10, id(x) = 4373172240
[初期] y = 10, id(y) = 4373172240
[変更後] x = 11, id(x) = 4373172272
[変更後] y = 10, id(y) = 4373172240
補足:ミュータブル型で同様のことを行うと
ミュータブル型の場合は「オブジェクトの内容が変更される」ので、両者に影響が及びます。
x = [1, 2]
y = x
x.append(3)
print(x) # [1, 2, 3]
print(y) # [1, 2, 3] ← y も影響を受ける(同じリストを参照しているため)
Q.3-5
リスト内の数値すべてをインプレースでインクリメントする for i in range(len(lst)): スタイルと、enumerate スタイルの違いを述べよ。
問題背景:リストの操作方法とループ構造
スタイル | 説明 |
---|---|
for i in range(len(lst)): |
明示的にインデックスを使ってアクセス・更新する |
for i, v in enumerate(lst): |
イテラブルから要素とインデックスを同時に取得(Python的で読みやすい) |
[1] for i in range(len(lst))
の特徴
・ リストの長さに基づいてloopする
・ 明示的にインデックス i
を用いて lst[i]
を操作する
・ すべての位置で要素の更新ができる
[2] enumerate(lst)
の特徴
・ 各loopで(index, 要素) のtupleを取得できる
・ 可読性が高く、index, valueの両方を自然に使いたい場合に最適
解答例とコードによる実践
これについては表にまとめた方が分かりやすい。
比較項目 | range(len(lst)) |
enumerate(lst) |
---|---|---|
可読性 | やや冗長で、C言語的 | シンプルで Pythonic |
柔軟性 | 要素の更新が明示的で細かく制御できる | インデックスと要素の両方が自然に使える |
推奨される用途 | 必ずインデックスが必要な場面(例:多重更新) | 通常はこちらが推奨される |
Python らしさ | あまり Python らしくない | Pythonic であり、標準的な書き方 |
# 【1】リストを定義
lst1 = [1, 2, 3, 4, 5]
lst2 = [1, 2, 3, 4, 5] # 比較用にもう1つコピー
# 【2】range(len()) を使ってインプレース更新
for i in range(len(lst1)):
lst1[i] += 1 # 【3】インデックスで直接要素を更新
# 【4】enumerate() を使ってインプレース更新
for i, v in enumerate(lst2):
lst2[i] = v + 1 # 【5】i番目の値に1を加えて代入(元のリストに変更を反映)
# 【6】結果表示
print(f"range(len()) スタイルの結果: {lst1}")
print(f"enumerate() スタイルの結果: {lst2}")
# 出力結果
range(len()) スタイルの結果: [2, 3, 4, 5, 6]
enumerate() スタイルの結果: [2, 3, 4, 5, 6]
Q.3-6
dict の全ての値に += 1 を行う処理において、キーエラーや None 値への対処法を提示せよ。
問題背景:dict
の値更新と例外への備え
[1] +=
演算による値の更新
・ my_dict[key] += 1
は、my_dict[key] = my_dict [key] + 1
と同義
・ その key
が存在しない場合、KeyError
が発生する
・ その key
に対応する値が None
の場合、None + 1
により TypeError
が発生する。
すなわち、以下の表に纏められる。
問題 | 発生条件 | エラータイプ |
---|---|---|
KeyError |
存在しないキーに += をしようとしたとき |
KeyError |
TypeError |
値が None など数値でない場合に ` |
[2] 上記Errorへの対処法
・ dict.get(key, default)
を用いて、安全に値を取得してから、+1
。
・ try-except
を用いて、値が None
や 非数値
のときにスキップor補正。
・ isinsance()
を用いて、数値であるかを明示的に確認
解答例とコードによる実践
文章による解答は上をまとめればよい。
# 【1】初期の辞書を定義。整数と None を混在させて例示。
my_dict = {'a': 1, 'b': None, 'c': 3, 'd': 'x', 'e': 0}
# 【2】すべてのキーに対して安全に += 1 を行う
for key in my_dict:
val = my_dict[key] # 【3】現在の値を取得
# 【4】値が int または float のときだけ加算
if isinstance(val, (int, float)):
my_dict[key] += 1 # 【5】値をインクリメント
else:
# 【6】None やその他の非数値型のときにはスキップ or 初期化
my_dict[key] = 1 # または: pass, または: logging.warning() など
# 【7】更新結果を表示
print("更新後の辞書:", my_dict)
# 出力結果
更新後の辞書: {'a': 2, 'b': 1, 'c': 4, 'd': 1, 'e': 1}
# (※ None や 'x' も 1 に置き換えた場合)
本文をまとめると以下。
問題点 | 対処法 |
---|---|
KeyError |
in や .get() でキーの存在確認をする |
None 値 |
isinstance(val, (int, float)) で数値型のみ加算する |
非数値(例: str) | 無視する / 1 に初期化 / ログ記録など |
ベストプラクティス | 「値の型チェックをしてから安全に代入」 が Pythonic |
補足:collections.defaultdict
を用いた例
from collections import defaultdict
d = defaultdict(int) # デフォルト値は 0
d['a'] += 1 # 存在しなくても自動的に 0 として扱われる
Q.3-7
ジェネレータ関数内で yield と i += 1 を組み合わせた際のステート管理の注意点を示せ。
問題背景:ジェネレータ関数と状態管理
[1] yield
の基本動作
yield
は値を一時的に返して関数の実行を一時停止させる。次に next()
を呼び出すと、yield
の次の行から実行が再開される。ジェネレータ関数は、中断と再開の状態(state)を内部に保持している。
[2] i += 1
を yield
と組み合わせる注意点
i += 1
を yield
の前に書くか後に書くかで出力の内容が変わる。stateの更新(i += 1
)が yield
の前にあるか後にあるかを明確に設定しないと、意図した値が出力されない。
これをまとめると、
・ジェネレータは「現在の実行位置と変数の状態を保存したまま一時停止できる関数」であり、yield
の前にi += 1
を書けば「インクリメントしてから値を出力」し、yield
の後にi += 1
を書けば、「今の値を出力してからインクリメント」する。
解答例とコードによる実践
ジェネレータは「現在の実行位置と変数の状態を保存したまま一時停止できる関数」であり、yield
の前にi += 1
を書けば「インクリメントしてから値を出力」し、yield
の後にi += 1
を書けば、「今の値を出力してからインクリメント」する。この際、以下の3点に注意する。
-
yield
前後での変数の更新順序を明確にする - 状態の更新が「再開後」に起きるのか、「再開前」に起きるのかが、出力値とstateの保持に影響する。
- ジェネレータでは、中断点の手前で状態を更新するか、再開後に更新するかでロジックが変わる。
位置関係 | 挙動と出力 | 説明 |
---|---|---|
i += 1 → yield i
|
1, 2, 3 | インクリメントしてから出力 |
yield i → i += 1
|
0, 1, 2 | 現在の値を出力してからインクリメント |
# 【1】インクリメント後に yield するジェネレータ関数
def gen_after_increment():
i = 0 # 【2】初期化
while i < 3:
i += 1 # 【3】まずインクリメント
yield i # 【4】インクリメント後の値を返す
# 【5】インクリメント前に yield するジェネレータ関数
def gen_before_increment():
i = 0 # 【6】初期化
while i < 3:
yield i # 【7】現在の i を返す
i += 1 # 【8】次のステップのためにインクリメント
# 【9】出力を比較
print("gen_after_increment():")
for val in gen_after_increment():
print(val) # → 1, 2, 3(インクリメントしてから出力)
print("gen_before_increment():")
for val in gen_before_increment():
print(val) # → 0, 1, 2(出力してからインクリメント)
# 出力例
gen_after_increment():
1
2
3
gen_before_increment():
0
1
2
Q.3-8
x += 1
を lambda
式内で使用できない理由を 論ぜよ。また、Python3.8以降に実装されたセイウチ演算子(:=
)を合わせて論じること。
問題背景:lambda
式と文(statement)の制限
[1] lambda
式について
lambda
は、無名関数を定義するための簡潔な構文で、lambda x: x + 1
のように、式(expression)のみを記述できる。
Python 公式ドキュメントにおいても、lambda
文は「単一の式のみ記述可」「文(assignment, if, for など)は記述不可」とされている。(https://docs.python.org/3/reference/expressions.html#lambda)
[2] x += 1
が lambda
式で利用できない理由
lambda x: x += 1
という入力に対しては、 SyntaxError: cannot assign to lambda
といった出力がなされる。x += 1
は式に見えても、Python の構文的には 「文(statement)」であり、代入文(Augmented Assignment Statement)に分類される。Python の lambda
式は、式(expression)しか含めることができないため、代入(=
, +=
など)を含むと構文エラーになる。
[3] セイウチ演算子(:=
)について
Python3.8で導入された代入式(assignment expression)であり、式として値を返しながら変数に代入できるという特徴がある。
# 代入と評価を同時に行う
if (n := len("hello")) > 3:
print(f"長さは {n} です") # 出力: 長さは 5 です
上記では len("hello")
を n
に代入しつつ、その値を用いて比較しています。通常の =
では、「代入しながら評価」を行うことはできない。
ここで、:=
は式(expression)であるため、lambda
式の中でも使用することが出来る。ただし、可読性に乏しいため、使用の際には注意が必要。
# 有効な例(構文としては正しい)
f = lambda x: (y := x + 1) # Python 3.8以降ならOK
# 実行
print(f(10)) # 出力: 11
この場合、y
に x + 1
の値を代入し、それを返す。
解答例とコードによる実践
lambda
式は、式のみを許容する関数リテラルであり、+=
は代入文であるから、式の一部として評価できないため構文上仕様でいない。一方で、:=
は代入と評価を同時に行う演算子であるから、lambda
式内においても利用可能である。
# 【1】lambda で代入を行うと SyntaxError になる(実行不可)
# invalid_lambda = lambda x: x += 1
# 【2】代わりに def を使って通常の関数を定義する
def add_one(x): # 【3】x を受け取る通常の関数
x += 1 # 【4】x に 1 を加算(これは文)
return x # 【5】新しい値を返す
# 【6】関数を呼び出して結果を表示
print(add_one(10)) # → 11
# 実行結果
11
セイウチ演算子(:=
)を用いて同様の実装を行いたいなら以下。
# 【1】セイウチ演算子を使って、lambda 内で代入と返却を同時に行う
add_one = lambda x: (x := x + 1) # 【2】x + 1 を計算し、x に代入しつつその値を返す
# 【3】関数を呼び出して結果を表示
print(add_one(10)) # → 11
あとがき
今回は「代入演算子」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話を中心に選んでみました。5問程度と書きつつも8問も扱いましたが、どれも重要と感じるものばかりです
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)