まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
今回も「if
による条件分岐」について扱います。それでは始めましょう!
Q.7-1
if–elif–else
の elif
部分に副作用のある関数を含む場合、評価の順序による影響を示せ。
問題背景
【1】if-elif-else
の評価順序
Python の if-elif-else
構文では、条件が順番に評価される。if
の条件が True
ならそのブロックが実行され、残りの elif
や else
は評価されない。if
が False
の場合、次に elif
の条件が評価され、すべての elif
が False
の場合に else
が実行される。
【2】副作用(side-effect)を持つ関数
関数に副作用があるとは、関数の実行が引数の変更や外部の状態の変更を引き起こすこと。print()
、listの append()
辞書の値の変更は副作用を持つ関数である。
本問であれば、elif
の中に副作用を持つ関数があると、その関数が評価されたタイミングで副作用が発生する。この時、条件分岐の評価順序により、副作用が発生するタイミングが異なることに注意しなければならない。
解答例とコードによる実践
# 【1】副作用のある関数を定義する(引数にリストを渡し、リストに値を追加する)
def side_effect_function(lst): # 【2】リストを引数に取り、リストに値を追加する関数
print("副作用が発生しました") # 【3】副作用の発生を確認するための出力
lst.append(100) # 【4】リストに値を追加する副作用
# 【5】リストを定義
my_list = [1, 2, 3] # 【6】最初のリストは [1, 2, 3] となる
# 【7】条件分岐を使って、副作用関数を `elif` に含める
x = 0 # 【8】x を 0 に設定(このため `if` 条件は False になる)
if x > 10: # 【9】x が 10 より大きい場合に実行される(今回は False)
print("x is greater than 10")
elif side_effect_function(my_list): # 【10】副作用関数を `elif` に含める
print("Side effect function executed")
else:
print("None of the conditions were true") # 【11】どの条件も True でなければこちらが実行される
# 【12】リストの最終的な状態を表示
print(f"最終的なリスト: {my_list}") # 【13】リストがどのように変化したか確認
副作用が発生しました
None of the conditions were true
最終的なリスト: [1, 2, 3, 100]
本文においては、以下のような評価順序を持つ
ステップ | 処理内容 | 説明 |
---|---|---|
【1】 |
x = 0 を代入 |
x は 0 になる |
【2】 |
if x > 10: を評価 |
0 > 10 は False なのでスキップされる |
【3】 |
elif side_effect_function(my_list): を評価 |
side_effect_function(my_list) が呼び出される(副作用発生) |
【4】 |
side_effect_function 内部で "副作用が発生しました" を出力 |
関数が実行される |
【5】 |
lst.append(100) が実行され、my_list に 100 が追加される |
my_list → [1, 2, 3, 100] に変化する |
【6】 |
side_effect_function の戻り値は None
|
None は False とみなされる |
【7】 |
elif 条件が False のため else 節が実行される |
"None of the conditions were true" が出力される |
【8】 | 最後に print(f"最終的なリスト: {my_list}") を実行 |
リストの内容 [1, 2, 3, 100] が出力される |
Q.7-2
if–elif–else
を match–case
に置き換えたときの構文と意味の違いを比較せよ。
問題背景
【1】if-elif-else
と match-case
構文
if-elif-else
は条件分岐を順番に評価し、最初に True
と評価された条件ブロックを実行する。
match-case
は Python 3.10 以降で導入された構文で、パターンマッチングを用いて複数の条件を評価し、条件に一致した場合にブロックを実行する。
【2】if-elif-else
の基本的な動作
各条件は順番に評価され、最初に True
になった条件に対応するブロックが実行される。
最初の if
が False
であれば、次に elif
が評価され、すべての elif
が False
の場合は else
が実行される。
【3】match-case の動作
`match` は与えられた変数の値をパターンと照合し、一致するパターンの `case` ブロックを実行する。
match-case
は switch
や case
文に似ており、if-elif-else
よりも直感的に複数の値のマッチを処理できる。
Python の match-case
では、値だけでなく構造的なパターンマッチング(例えばリストやタプルのパターン)も可能。
すなわち、
・ if-elif-else
の場合、各条件を順番に評価し、一致した場合にそのブロックが実行される。
・ match-case
の場合、与えられた値と一致するパターンを探し、一致したパターンの case
ブロックが実行されます。
・ match-case
は、特定の値に対する多くのパターンを整理して表現するのに非常に便利。
解答例とコードによる実践
【A】if-elif-else
の場合
# 【1】整数 x を定義
x = 2 # 【2】x の値を 2 に設定
# 【3】if-elif-else の条件分岐を設定
if x == 1: # 【4】x が 1 の場合
print("x is 1") # 【5】x が 1 の場合に実行される
elif x == 2: # 【6】次に x が 2 の場合
print("x is 2") # 【7】x が 2 の場合に実行される
elif x == 3: # 【8】次に x が 3 の場合
print("x is 3") # 【9】x が 3 の場合に実行される
else: # 【10】上記の条件に一致しない場合
print("x is something else") # 【11】それ以外の値のとき
# 出力結果
x is 2
【B】match-case
の場合(Python 3.10以降)
# 【1】整数 x を定義
x = 2 # 【2】x の値を 2 に設定
# 【3】match-case のパターンマッチングを使用
match x: # 【4】x の値と一致するパターンを探す
case 1: # 【5】x が 1 の場合
print("x is 1") # 【6】x が 1 の場合に実行される
case 2: # 【7】x が 2 の場合
print("x is 2") # 【8】x が 2 の場合に実行される
case 3: # 【9】x が 3 の場合
print("x is 3") # 【10】x が 3 の場合に実行される
case _: # 【11】それ以外の場合(デフォルトケース)
print("x is something else") # 【12】それ以外の値のとき
x is 2
本文のポイントをまとめると以下。
観点 | if-elif-else |
match-case |
---|---|---|
構文 | 条件を順番に評価し、最初に一致したブロックを実行 | 値をパターンに基づいて比較し、一致するパターンの case を実行 |
評価順序 | 条件が順番に評価され、最初に True になるものが実行される |
最初に一致する case が実行され、他の case は無視される |
柔軟性 |
elif を使って多くの条件を順番に評価 |
複数の条件に対して直接的なパターンマッチングが可能 |
使用例 | 連続する範囲や単純な条件分岐に向いている | たくさんの値に対するマッチングや複雑なパターンに便利 |
Q.7-3
多段の if–elif
構造に対して、可読性と拡張性の観点から関数分割を用いた設計にリファクタリングせよ。また、 dict
と lambda
を用いて関数的に処理する構造を作成せよ。また、これらの使い分けの設計指針について示せ。
問題背景
【1】if-elif-else
と可読性・拡張性
可読性:if-elif-else
の分岐が多くなると、コードが長くなり、条件ごとの処理が分かりづらくなる可能性がある。
拡張性:条件が増えるたびに elif
を追加していくと、コードが複雑化し、新たな条件を追加するたびに全体の見通しが悪くなる。
【2】関数分割による設計の改善
関数分割:処理を関数に分けることで、各条件ごとの処理を個別に定義でき、コードの可読性と再利用性を向上させる。
条件を評価する関数を作り、その関数を if-elif-else
の中で呼び出すことで、条件判定の部分を簡潔に保つことができる。
【3】関数的処理
辞書を用いて条件分岐を関数的に処理することで、コードを 簡潔に保ち、柔軟に拡張できるようになる。
新しい条件を追加する場合、辞書に新たなキーと関数を追加するだけで済む。例えば今回は、lambda
を使うことで、辞書の値に関数を設定し、辞書を使って条件ごとに対応する関数を呼び出すことが出来る。
解答例とコードによる実践
【A】if-elif-else
の場合(非リファクタリング)
# 【1】整数 x を定義
x = 2 # 【2】x の値を 2 に設定
# 【3】if-elif-else の条件分岐を設定
if x == 1: # 【4】x が 1 の場合
print("x is 1") # 【5】x が 1 の場合に実行される
elif x == 2: # 【6】次に x が 2 の場合
print("x is 2") # 【7】x が 2 の場合に実行される
elif x == 3: # 【8】次に x が 3 の場合
print("x is 3") # 【9】x が 3 の場合に実行される
elif x == 4: # 【10】次に x が 4 の場合
print("x is 4") # 【11】x が 4 の場合に実行される
else: # 【12】上記の条件に一致しない場合
print("x is something else") # 【13】それ以外の値のとき
【B】関数分割によるフィルタリング
# 【1】x の値を設定
x = 2 # 【2】x の値を 2 に設定
# 【3】各条件に対応する処理を関数として定義
def handle_x_is_1(): # 【4】x が 1 の場合の処理
print("x is 1")
def handle_x_is_2(): # 【5】x が 2 の場合の処理
print("x is 2")
def handle_x_is_3(): # 【6】x が 3 の場合の処理
print("x is 3")
def handle_x_is_4(): # 【7】x が 4 の場合の処理
print("x is 4")
def handle_default(): # 【8】どの条件にも一致しない場合の処理
print("x is something else")
# 【9】if-elif-else の中で関数を呼び出す
if x == 1: # 【10】x が 1 の場合
handle_x_is_1() # 【11】関数を呼び出して処理を実行
elif x == 2: # 【12】x が 2 の場合
handle_x_is_2() # 【13】関数を呼び出して処理を実行
elif x == 3: # 【14】x が 3 の場合
handle_x_is_3() # 【15】関数を呼び出して処理を実行
elif x == 4: # 【16】x が 4 の場合
handle_x_is_4() # 【17】関数を呼び出して処理を実行
else: # 【18】上記の条件に一致しない場合
handle_default() # 【19】関数を呼び出して処理を実行
【C】dict
と lambda
を使った関数的処理
# 【1】x の値を定義
x = 2 # 【2】x の値を 2 に設定
# 【3】条件に対応する処理を lambda 関数で定義し、辞書に登録
handlers = {
1: lambda: print("x is 1"), # 【4】x が 1 の場合の処理
2: lambda: print("x is 2"), # 【5】x が 2 の場合の処理
3: lambda: print("x is 3"), # 【6】x が 3 の場合の処理
}
# 【7】辞書で x に対応する関数を実行(x が辞書にない場合は default を使う)
handlers.get(x, lambda: print("x is something else"))() # 【8】x が辞書にあればその関数を実行し、なければ default を実行
x is 2
関数分割によるリファクタリングとdict
& lambda
による関数的処理を比較すると、
観点 | 関数分割によるリファクタリング |
dict & lambda による関数的処理 |
---|---|---|
可読性 | 条件ごとの処理が個別の関数に分かれるため、長い処理でも読みやすい | 条件が多い場合でも簡潔に表現できるが、処理が複雑だと lambda では読みにくくなる場合がある |
拡張性 | 新しい条件追加時に関数を追加するだけで済む | 辞書に新しいキーと lambda を追加するだけで済む |
保守性 | 各関数が独立しており、単体テストもしやすい | 短い処理ならシンプルだが、処理が複雑なら関数化した方が良い場合もある |
柔軟性 | 関数内で複数行の複雑なロジックが書ける | 基本的に1行処理向き (lambda は複数行NG) |
動的選択 | 基本的には静的な条件分岐 | 条件名(キー)を変数で動的に指定できる |
適用範囲 | 複雑な処理や長い処理が必要な場合に向いている | 条件ごとの処理がシンプルで、条件が数値や文字列などの値によるマッチングの場合に向いている |
設計指針をまとめると、以下。
・ 最初は dict
& lambda
で簡単に書く
➡ 条件が増えたり、処理が複雑になったら関数に切り出して分割するという流れが現実的。
・ lambda
に無理に複雑なことを書かない
➡ 可読性が落ちたり、バグの温床になりやすい。
・テストを考慮する場合は関数分割が強い
➡ 関数ごとに単体テストを書きやすい。
Q.7-4
if–elif–else
の構造をオブジェクト指向的に整理し、クラスで分岐処理を切り替える方法を提示せよ。
問題背景
【1】if-elif-else
の構造と課題
if-elif-else
は簡単な条件分岐には適していますが、分岐が増えると可読性が悪くなり、拡張性・保守性が下がる。
分岐ごとの処理が複雑な場合、それぞれの処理をクラスごとに切り出すことでコードが整理され、オブジェクト指向の利点(拡張性、柔軟性、テストのしやすさ)を活かすことができる。
【2】オブジェクト指向設計(ポリモーフィズムの活用)
「条件ごとに処理を切り替える」という設計は、ポリモーフィズム(同じインターフェースで異なる動作)で実現できる。共通の基底クラス(スーパークラス)を用意し、条件ごとの処理を各サブクラスでオーバーライドする。
例えば、各条件(例えば "start"
, "stop"
, "pause"
)ごとに異なる処理をしたい場合、if-elif-else
ではすべてを1つの関数に書きがち。クラス設計を用いることで、条件ごとの処理を 個別のクラスに分けて管理できる。この設計なら、条件が増えてもコードの構造が崩れず、後から新しい分岐を追加するのも簡単。
解答例とコードによる実践
# 【1】基底クラス(スーパークラス)を定義する
class CommandHandler:
# 【2】すべての処理クラスで共通の execute メソッドを定義(ポリモーフィズムのため)
def execute(self):
raise NotImplementedError("サブクラスで execute() を実装してください")
# 【3】"start" コマンドの処理クラス
class StartHandler(CommandHandler):
def execute(self):
print("処理を開始します。") # 【4】start に対応する処理
# 【5】"stop" コマンドの処理クラス
class StopHandler(CommandHandler):
def execute(self):
print("処理を停止します。") # 【6】stop に対応する処理
# 【7】"pause" コマンドの処理クラス
class PauseHandler(CommandHandler):
def execute(self):
print("処理を一時停止します。") # 【8】pause に対応する処理
# 【9】"unknown" コマンドの処理クラス(デフォルト処理)
class UnknownHandler(CommandHandler):
def execute(self):
print("不明なコマンドです。") # 【10】条件に一致しない場合の処理
# 【11】コマンドとハンドラの対応関係を辞書で定義
command_map = {
"start": StartHandler,
"stop": StopHandler,
"pause": PauseHandler,
}
# 【12】ユーザー入力を受け取る(CLI の入力)
user_input = input("コマンドを入力してください (start/stop/pause): ") # 【13】CLI で入力を受け取る
# 【14】適切なハンドラを取得(存在しない場合は UnknownHandler)
handler_class = command_map.get(user_input, UnknownHandler) # 【15】辞書からクラスを取得
# 【16】ハンドラをインスタンス化
handler_instance = handler_class() # 【17】クラスを実体化(オブジェクト生成)
# 【18】ハンドラの処理を実行
handler_instance.execute() # 【19】適切なクラスの execute メソッドが実行される(ポリモーフィズム)
# 実行結果
コマンドを入力してください (start/stop/pause): start
処理を開始します。
コマンドを入力してください (start/stop/pause): stop
処理を停止します。
コマンドを入力してください (start/stop/pause): hello
不明なコマンドです。
今回のコードであれば、
・ command_map
辞書が key
(コマンド文字列)と class
の対応表になっており、条件ごとに if-elif-else
で分岐せずに済んでいる。
・ 各処理は独立したクラスとして実装されているため、1つ1つの処理内容が分かりやすい。
・ 新しいコマンド "resume"
などを追加する場合も、新しい ResumeHandler
classを追加し、command_map
に key
を追加するだけで済む。
本問についてまとめると以下
項目 | 内容 |
---|---|
可読性 | 分岐ごとの処理をクラス単位で分離でき、スッキリする |
拡張性 | 新しい処理はクラスを追加するだけで済む |
保守性 | 各処理クラスが独立しており、単体テストしやすい |
ポリモーフィズム | 条件ごとの分岐を if-elif でなく、動的に切り替え可能 |
Q.7-5
ネストされた if
の中で、ループ変数を使った場合にスコープの衝突が発生しない理由を for
文との関係から説明せよ。
問題背景
【1】スコープ(変数の有効範囲)
スコープとは、変数が有効である範囲を指し、Python では、変数はその定義されたブロック内でのみ有効。
ローカルスコープ:関数やループなどのブロック内で定義された変数は、そのブロック内でのみ有効。
グローバルスコープ:関数外で定義された変数は、プログラム全体でアクセス可能。
【2】for
ループとスコープ
for
文 で変数を定義する場合、その変数は for
ループのスコープ内で使えるが、ループが終わるとその変数は次のループで上書きされる。
これは、for
ループの外でも変数が参照できるため、ループ内で新たに値が代入されるたびに前回の値が「上書き」されるため。
【3】ネストされた if
文
ネストされた if
文は、ある if
文の中に別の if
文が含まれる構造だが、これは基本的に同じスコープ内で評価される。
ループ変数(for
の変数)が if
文内で使用されていても、スコープが衝突しないのは、ループ内で新しい変数が常に上書きされていくため。
解答例とコードによる実践
for
ループの変数はそのスコープ内のみで用いられるため、ネストされた if
文やループ内で使用しても、スコープが自動的に管理される。すなわち、for
ループの各反復において変数が上書きされるため、スコープの衝突は発生しない。
# 【1】リストを定義
numbers = [1, 2, 3, 4] # 【2】ループで使うリストを定義
# 【3】for ループで数値を順番に処理
for num in numbers: # 【4】リストから順番に数値を取り出す
print(f"ループ変数: {num}") # 【5】現在のループ変数 num を表示
# 【6】条件による分岐
if num % 2 == 0: # 【7】num が偶数かどうかをチェック
print(f"{num} は偶数です") # 【8】偶数の場合、メッセージを表示
# 【9】さらにネストされた if 文
if num > 2: # 【10】num が 2 より大きいかをチェック
print(f"{num} は 2 より大きい") # 【11】2 より大きい場合、メッセージを表示
# 出力結果
ループ変数: 1
ループ変数: 2
2 は偶数です
ループ変数: 3
3 は偶数です
3 は 2 より大きい
ループ変数: 4
4 は偶数です
4 は 2 より大きい
Q.7-6
if
文のネストが2段階以上あるコードで、return
を使って早期終了する設計と、ネストを使う設計を比較せよ。
問題背景
【1】 if
文のネスト
Pythonでは、 if
文をネストにして複雑な条件分岐を記述できるが、ネストが深くなると可読性が低下し、バグの原因や保守性の低下につながる。
【2】 return
による早期終了(early return)
return
を用いることで条件を満たした時点で処理を中断し、以降のコードの実行をスキップできる。早期終了は「ガード節」とも呼ばれ、ネストを浅く保ち、見通しの良いコードを書くための手法としてよく用いられる。
纏めると、
同じ処理を行う場合でも、
・ ネストを深くして条件分岐を書く方法
・ return
を用いて早期終了する方法
があり、後者はコードの複雑さを減らして、可読性が向上させる。
解答例とコードによる実践
【A】 ネストを用いる場合
# 【1】関数を定義する
def check_number_nested(x): # 【2】x の値を受け取り条件判定する関数
if x > 0: # 【3】x が正の数かどうかを判定
if x % 2 == 0: # 【4】x が偶数かどうかを判定
print("正の偶数です") # 【5】条件を満たす場合メッセージを表示
else: # 【6】x が正の奇数の場合
print("正の奇数です") # 【7】メッセージを表示
else: # 【8】x が 0 または負の数の場合
print("0 または負の数です") # 【9】メッセージを表示
# 【10】関数を呼び出して動作確認
check_number_nested(4)
【B】 return
を用いた早期終了(推奨設計)
# 【1】関数を定義する
def check_number_early_return(x): # 【2】x の値を受け取り条件判定する関数
if x <= 0: # 【3】x が 0 以下ならすぐに終了
print("0 または負の数です") # 【4】条件を満たす場合メッセージを表示
return # 【5】処理をここで終了
if x % 2 == 0: # 【6】x が偶数の場合
print("正の偶数です") # 【7】メッセージを表示
return # 【8】処理を終了
print("正の奇数です") # 【9】残るのは正の奇数のみ(条件を満たさない場合)
# 【10】ここで終了(暗黙の return None)
# 【11】関数を呼び出して動作確認
check_number_early_return(4)
# 実行結果(x = 4 の場合)
正の偶数です
本問についてまとめると
観点 | ネストあり設計 | 早期終了(return )設計 |
---|---|---|
可読性 | ネストが深くなり読みにくい | 条件が浅くなり読みやすい |
保守性 | 条件追加時にネストが増えやすい | 条件追加も容易、ネストが浅く保てる |
バグの可能性 | 条件ミスでブロックが実行されない可能性 | 条件を満たした時点で終了するため安全 |
パフォーマンス | 無駄な条件評価が残る | 不要な処理を即座にスキップできる |
あとがき
今回も「if
による条件分岐」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話を中心に選んでみました。あと1回、「if
による条件分岐」を扱います。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)