まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
2000問ほどあるストックを問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
今回は「ミュータブルとイミュータブル」に加えて、以前に別に扱うとしていた「値渡しと参照渡し」について扱います。それでは始めましょう!
Q.4-1
int
, float
, str
, tuple
がイミュータブルであり、list
, dict
, set
がミュータブルである理由を説明せよ。
問題背景:イミュータブル型とミュータブル型の本質的違い
これは以下のように表に纏めてしまうのが分かりやすい。
用語 | 定義 |
---|---|
イミュータブル(immutable) | 生成後に内容を変更できないオブジェクト型。値を変えるには新しいオブジェクトが必要。 |
ミュータブル(mutable) | 生成後も内容を変更可能なオブジェクト型。同じオブジェクトを保持したまま変更できる。 |
解答例とコードによる実践
各型がそれぞれミュータブル/イミュータブルに属する理由を以下に示す。
int
, float
, str
, tuple
がイミュータブルである理由
・ hashability(ハッシュ可能性)を持つため
e.g.) 辞書の key
や集合の要素に用いることが出来る(変更不可能である必要がある)
・ オブジェクトの一貫性と安全性が重視されているため
➡ 共有されても予期せぬ副作用が起こらない
・ 構造的に単純である
➡ 内部的な状態を変更する必要がない
list
, dict
, set
がミュータブルである理由
・ 実用性と柔軟性を優先した設計
➡ サイズや要素の追加/削除/更新が必要なデータ構造
・ パフォーマンスの観点から、再生成せずにその場での変更が可能
・ 順序や対応関係(key
と value
)の操作が頻繁に必要
# 【1】イミュータブル型(int)の例
x = 10
print(f"[int] 初期 id(x): {id(x)}") # 【2】x のオブジェクトIDを表示
x += 1 # 【3】x に 1 を加算(新しいオブジェクトが生成される)
print(f"[int] 変更後 id(x): {id(x)}") # 【4】IDが変わる(別オブジェクト)
# 【5】ミュータブル型(list)の例
lst = [1, 2, 3]
print(f"[list] 初期 id(lst): {id(lst)}") # 【6】リストのIDを表示
lst.append(4) # 【7】リストに要素を追加(オブジェクト自体を変更)
print(f"[list] 変更後 id(lst): {id(lst)}") # 【8】IDは変わらない(同じオブジェクト)
print(f"[list] 中身: {lst}") # 【9】リスト内容の確認
# 出力結果
[int] 初期 id(x): 4373172240
[int] 変更後 id(x): 4373172272
[list] 初期 id(lst): 140088319812416
[list] 変更後 id(lst): 140088319812416
[list] 中身: [1, 2, 3, 4]
Q.4-2
a = [1, 2, 3]; b = a; a.append(4)
の後、b
の中身に変化があるかを確認し、その挙動の理由を説明せよ。 また、a = [1, 2, 3]; b = a.copy(); a.append(4)
の場合、b
の値は変わらない理由を copy()
の挙動から説明せよ。
問題の背景:代入とcopy
の違い(参照の共有と複製)
[1] b = a
の場合:参照をコピー(同じオブジェクトを指す)
b = a
はlist a
の中身をコピーするのではなく、a
参照(ポインタ)をコピーして b
に渡す。従って、a.append(4)
によるlistの変更は b
にも影響する。
[2] b = a.copy()
の場合:shallow copy(新しいリストオブジェクトの生成)
a.copy()
は新しいリストを生成し、元のリスト a
と同じ要素を持つが別オブジェクトになる。したがって、a.append(4)
による変更は、コピー先 b
には影響しない。
解答例とコードによる実践
解答としては、上記の背景を表に纏めて示す。
操作 | 概要説明 | 結果 |
---|---|---|
b = a |
a の参照を b に代入。同じオブジェクトを共有 |
a も b も一緒に変化する |
b = a.copy() |
a の内容をコピーして別のオブジェクトにする |
b は a の変更と無関係 |
# 【1】リスト a を定義
a = [1, 2, 3]
# 【2】a を b に代入(参照を共有)
b = a
# 【3】a に 4 を追加(リストの内容を変更)
a.append(4)
# 【4】a と b の中身を確認(どちらも同じオブジェクトを参照している)
print(f"[参照共有] a = {a}") # → [1, 2, 3, 4]
print(f"[参照共有] b = {b}") # → [1, 2, 3, 4]
print("---")
# 【5】リスト a を再定義
a = [1, 2, 3]
# 【6】a をシャローコピーして b に代入(別オブジェクトを作る)
b = a.copy()
# 【7】a に 4 を追加
a.append(4)
# 【8】a と b の中身を確認(b は元のまま)
print(f"[copy使用] a = {a}") # → [1, 2, 3, 4]
print(f"[copy使用] b = {b}") # → [1, 2, 3]
# 出力結果
[参照共有] a = [1, 2, 3, 4]
[参照共有] b = [1, 2, 3, 4]
---
[copy使用] a = [1, 2, 3, 4]
[copy使用] b = [1, 2, 3]
Q.4-3
copy.copy()
と copy.deepcopy()
の対象がイミュータブルかミュータブルかで、結果にどのような差が出るかを検証せよ。
問題背景:copy.copy()
と copy.deepcopy()
の動作と違い
[1] copy.copy()
(浅いコピー)
オブジェクト本体のみ複製するが、その中にある要素(リストや辞書など)は参照を共有する。イミュータブル型に使っても効果がない(そもそも変更できないため)。
[2] copy.deepcopy()
(深いコピー)
オブジェクト本体に加えて、その中に含まれるオブジェクトも再帰的に複製される。イミュータブル型ではやはり変化なし(コピーしても共有しても実質的に違いが出ない)。
これを纏めると、
イミュータブル型の場合(例:int
, str
, tuple
)
どちらを用いても、オブジェクトIDは同じでなることが多い。これはPythonの最適化により、同じ値のイミュータブルは再利用されることがある。
ミュータブル型の場合(例:list
, dict
)
copy.copy()
は外側だけがコピーされるため、中身の要素が共有される。一方で、copy.deepcopy()
は中身も再帰的にコピーされ完全に独立した構造が得られる。特にネスト構造を持つミュータブルなオブジェクトを扱う際には、deepcopy()
を用いて独立性を確保し、変更が予期せぬ箇所に波及するバグを防ぐようにしなければならない。
Q-4.4
any()
や all()
と組み合わせて := を使うことで、最初に真となる値を保持する実装例を作成せよ。
問題背景:any()
/ all()
と :=
(代入式)の併用
[1] セイウチ演算子(:=
)の意義
Python 3.8で導入された演算子で、代入と評価を同時に行うことが出来る。式の中で用いることが出来るため、any()
, all()
などのイテレータ評価と変数保持を1ステップで行うことが出来る。
[2] any()
/ all()
これは以下の表で話題は尽きる。
関数 | 判定 | 戻り値 |
---|---|---|
any() |
少なくとも1つが真なら True
|
最初に True になった時点で評価を止める(短絡評価) |
all() |
全てが真なら True
|
最初に False になった時点で評価を止める(短絡評価) |
解答例とコードによる実装例
# 【1】対象の文字列リストを定義
data = ["cat", "dog", "elephant", "fox", "hippopotamus"]
# 【2】最初に len(s) > 5 を満たす文字列を見つけて保持(Python 3.8 以降で有効)
if any((long_word := s) for s in data if len(s) > 5):
# 【3】条件を満たす最初の要素を表示(保持されている)
print(f"最初に見つかった長い単語: {long_word}")
else:
print("条件を満たす単語は見つかりませんでした")
# 出力結果
最初に見つかった長い単語: elephant
上記の例であれば、リストの中から最初に len(s) > 5
を満たす文字列を探し、その文字列を変数に格納しておきたい。
➡ for
ループ+break で書くより、any()
+ :=
の方が短くかつ明確。
# 【1】文字列のリストを定義する。空文字列が途中に含まれている。
data = ["apple", "banana", "", "cherry"]
# 【2】all() を使って、すべての文字列が「真」かどうかを判定する。
# セイウチ演算子 (:=) を用いて、各要素 s を item に代入しながら評価する。
# 空文字列 '' は偽と判定されるため、最初に見つかると all() は False を返す。
# 条件 not all(...) により、1つでも偽(空文字列)があると if 節が実行される。
if not all((item := s) for s in data):
# 【3】all() の途中で偽と判定された値(空文字列)を item として保持しているので、表示する。
print(f"最初に空文字列を検出しました: {item}")
else:
# 【4】すべての要素が非空(すなわち真)であればこちらが実行される。
print("すべての要素が非空です")
# 出力結果
最初に空文字列を検出しました:
Q.4-5
lambda x: x if x > 0 else -x のような条件演算子を含むラムダ式を作成し、使い所を述べよ。
問題背景:lambda
式と条件演算子
[1] lambda
式
lambda
式は無名関数を定義するための構文であり、lambda x: x + 1
のように、1行で完結する簡単な処理を記述するために使用される。
[2] 条件演算子(x if conditon else y
)
条件演算子は、if-else
文の簡略化として用いる。condition
が真であれば x
を返し、偽なら y
を返すという構造を持つ。構文は以下。
result = x if condition else y
[3] lambda
式と条件演算子を組み合わせる
条件演算子を lambda
の式内に組み込むことで、短く効率的な関数を定義できる。
解答例とコードによる実践
lambda x: x if x > 0 else -x
というコードは、数値が 負の場合に絶対値を求めるための簡易的な関数として使うことができる。
上記の関数は、数値が負のときにその符号を反転したい場合や、map
関数などで配列やリストに対して処理を簡潔に行いたい場合に有効。
# 【1】負の値を反転するラムダ式を定義
absolute_value = lambda x: x if x > 0 else -x # 【2】x が正ならそのまま、負なら絶対値にする
# 【3】関数を使って、いくつかの値を処理
numbers = [-5, 3, -2, 7, -1]
# 【4】map() を使用して、リストの各要素にラムダ式を適用
result = list(map(absolute_value, numbers)) # 【5】各要素を絶対値に変換
# 【6】結果を表示
print(f"変換前のリスト: {numbers}") # → [-5, 3, -2, 7, -1]
print(f"変換後のリスト: {result}") # → [5, 3, 2, 7, 1]
# 出力結果
変換前のリスト: [-5, 3, -2, 7, -1]
変換後のリスト: [5, 3, 2, 7, 1]
Q.4-6
条件演算子を使ったリスト内包表記を作成し、値のフィルタリングと変換を同時に行う処理を示せ。
問題背景:条件演算子とリスト内包表記
[1] リスト内包表記(List Comprehension)
リスト内包表記:既存のリストやイテラブルから新しいリストを効率的に生成する構文。構文例は以下。
[expression for item in iterable if condition]
expression
:各要素に対して行いたい操作
iterable
:処理するものとイテラブル(list, tuple, ...)
condition
:フィルタリング条件(オプション)
解答例とコードによる実践例
# 【1】元のリストを定義
numbers = [1, 2, -3, 4, -5, 6]
# 【2】リスト内包表記を使い、負の数を無視し、正の数を2倍に変換する
result = [n * 2 if n > 0 else None for n in numbers]
# 【3】None 値を除外するため、さらにフィルタリングしてから結果を表示
filtered_result = [n for n in result if n is not None]
# 【4】最終的な結果を表示
print(filtered_result) # → [2, 4, 8, 12]
# 実行例
[2, 4, 8, 12]
あとがき
今回は「ミュータブルとイミュータブル」、「値渡しと参照渡し」に加えて、演算子についても追加で難問か扱いました。個人的には文法理解に資する6問を選んだつもりです。次回は「format文とf-string」について扱います
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)