まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
今回も「if
による条件分岐」について扱います。それでは始めましょう!
Q.8-1
if
のネストを match-case
に変換できる条件構造を挙げ、構文上の違いを論ぜよ。
問題背景
【1】 if
のネストとその問題点
if
文のネストは、条件が複雑になればなるほど可読性が悪化し、条件分岐の追跡も困難になる。特に多層的に条件分岐が続く場合、処理の流れも直観的でなくなる。また、仕様変更により条件に相当する箇所のコードを変更した際に、バグが発生する可能性も高くなる。
【2】 match-case
によるパターンマッチング
match-case
はパターンマッチングを行い、複雑な条件分岐をシンプルに表現できる。match
は変数と値をパターンで比較し、対応するcase
によって分岐する。一致するパターンが見つかると、そのブロックを実行し、最初に一致した case
のみが実行され、残りの case
は無視される。
【3】 if
文から match-case
への変換可能な構造
if
文が値を比較する、または複数の条件をチェックする場合、match-case
への変換が可能。特に、複数の値に基づく条件分岐やパターンマッチの際に有効。一方で、複雑な論理式や数値範囲の比較には適さないことに注意。
解答例とコードによる実践
【A】 if
文のネストの例
# 【1】変数 x を定義
x = 2 # 【2】x の値を 2 に設定
# 【3】if 文を使ったネストされた条件分岐
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】それ以外の条件(x が 1, 2, 3 以外の場合)
print("x is something else") # 【11】それ以外の場合、メッセージを表示
【B】 match-case
による書き換え
# 【1】変数 x を定義
x = 2 # 【2】x の値を 2 に設定
# 【3】match-case を使った条件分岐
match x: # 【4】x を match で評価
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
Q.8-2
入れ子の if
文中に try-except
を使う構造と、外側に try
を置く構造の違いをエラー処理観点から比較せよ。
問題背景
【1】 try-except
による例外処理
Pythonでは、予期しないエラー(例外)が発生した場合、プログラムが停止する。これを防ぐために try-except
文を用いて、エラーを補足(catch)して適切な処理を行うことが出来るようにする。
【2】 ネスト内の try-except
VS ネスト外の try-except
ネスト内側に try-except
を置くと、特定の条件を満たす場合のみエラー処理を行うという設計になる。この場合、必要最小限の範囲で try-except
を用いることで、想定していない箇所のエラーは補足しない。
一方、ネスト外側に try-except
を置くと、すべての処理に対して一括でエラーを補足する。エラー発生個所を特定しづらくなる場合もあるが、全体を安全に動作させたい場合に有効。
解答例とコードによる実践
【A】 ネストの if
の内側に try-except
を用いる場合(局所的な例外処理)
# 【1】関数を定義
def check_and_divide_inner(num): # 【2】num を受け取る関数
if num > 0: # 【3】num が正のときのみ処理を行う
try: # 【4】エラーが起こりそうな部分を局所的に囲む
result = 10 / num # 【5】ゼロ除算の可能性がある
print(f"結果は {result} です") # 【6】正常時の出力
except ZeroDivisionError: # 【7】ゼロ除算のエラーを捕捉
print("0 で割ることはできません") # 【8】エラー時のメッセージ
else: # 【9】num が 0 以下の場合
print("正の数を入力してください") # 【10】条件が合わない場合の処理
# 【11】動作確認
check_and_divide_inner(0)
# 実行結果
正の数を入力してください
【B】 ネストの if
の外側に try-except を置く場合(広範囲の例外処理)
# 【1】関数を定義
def check_and_divide_outer(num): # 【2】num を受け取る関数
try: # 【3】すべての処理を囲む
if num > 0: # 【4】num が正のときのみ処理を行う
result = 10 / num # 【5】ゼロ除算の可能性がある
print(f"結果は {result} です") # 【6】正常時の出力
else: # 【7】num が 0 以下の場合
print("正の数を入力してください") # 【8】条件が合わない場合の処理
except ZeroDivisionError: # 【9】ゼロ除算のエラーを捕捉
print("0 で割ることはできません") # 【10】エラー時のメッセージ
# 【11】動作確認
check_and_divide_outer(0)
# 実行結果
正の数を入力してください
本文をまとめると以下。
観点 | 入れ子内 try-except
|
外側 try-except
|
---|---|---|
局所性 | 必要な箇所だけエラー処理できる | どの処理でエラーが出たか分かりにくい |
可読性 | 明確にどの処理が危険か分かる | 広範囲を囲むと処理の範囲が見えにくい |
保守性 | エラー箇所を絞って安全に修正できる | 小さな変更でも全体に影響を与える可能性がある |
パフォーマンス | エラーチェックは最小限 | 無駄な try 範囲が増える可能性あり |
用途 | 一部の処理だけ例外が出そうな場合 | 広い範囲で例外が出る可能性がある場合 |
Q.8-3
if
ネストが深くなった結果、見通しが悪くなる設計例を挙げ、関数分割による改善案を提示せよ。また、このケースにおいて、if
文をクラスによって分離した場合の設計と、関数だけで構築した場合の比較を行え。
問題背景
【1】 関数定義( def
)と責務分割
関数は処理をまとめるための単位であり、def
を用いて定義する。
def process_standard_credit():
return 500
関数分割によって、
・ ネストを浅くできる。
・ 各処理内容が関数名によって説明されるため(そのような関数名にするべきである)、読めば何をしているかの把握が可能。
・ テストやデバックが容易。
例えば、
・ 悪い例(ネストが深い)
if delivery == 'standard':
if payment == 'credit':
return 500
・ 良い例(関数分割)
def standard_credit_fee():
return 500
if delivery == 'standard':
return standard_credit_fee()
【2】 クラス定義( class
)とカプセル化
Pythonのクラスは、データとその操作を1つにまとめた構造である。
class StandardFee:
def calculate(self, payment):
if payment == 'credit':
return 500
カプセル化とは、「何をするか(インターフェイス)」と「どうするか(実装)」を分ける設計思想のこと。実装の詳細を隠し、利用者には共通のメソッド( calculate()
など)だけを公開する。
【3】ポリモーフィズム
複数の異なるクラスが同じメソッド名を持ち、共通のインターフェースで呼び出せる性質をポリモーフィズム(多態性)と呼ぶ
class StandardFee:
def calculate(self, payment):
# 実装
class ExpressFee:
def calculate(self, payment):
# 実装
# 呼び出し側は calculate() を意識すればよい
strategy = StandardFee()
fee = strategy.calculate('credit')
この仕組みにより、呼び出し側はクラスの中身を知らなくても適切に動作でき、拡張性が向上する。
【4】 戦略パターン(Strategy Pattern)
ポリモーフィズムを応用した設計パターンの1つであり、「アルゴリズム(本文では料金計算)」
これを用いることで、以下のメリットを得る
・ if
文でアルゴリズムを切り替える必要がなくなる
・ 処理の追加が容易(クラスを追加するだけ)
・ 保守性/拡張性が高い
解答例とコードによる実践
【A】 ネストが深い悪い設計例
# 【1】配送方法と支払い方法で手数料を計算する関数
def calculate_fee(delivery, payment):
# 【2】配送方法で分岐
if delivery == 'standard':
# 【3】標準配送時、支払い方法でさらに分岐
if payment == 'credit':
return 500
else:
if payment == 'cash':
return 600
else:
return 700
else:
if delivery == 'express':
# 【4】速達配送時、支払い方法でさらに分岐
if payment == 'credit':
return 800
else:
if payment == 'cash':
return 900
else:
return 1000
else:
return 1200 # 【5】配送方法が不明な場合
# 【6】計算結果の表示
print(calculate_fee('standard', 'credit')) # 【7】500が出力される
【B】 関数分割による設計(Pythonicな改善)
# 【1】標準配送の手数料を計算する関数
def standard_fee(payment):
# 【2】支払い方法で手数料を返す
if payment == 'credit':
return 500
elif payment == 'cash':
return 600
else:
return 700
# 【3】速達配送の手数料を計算する関数
def express_fee(payment):
# 【4】支払い方法で手数料を返す
if payment == 'credit':
return 800
elif payment == 'cash':
return 900
else:
return 1000
# 【5】配送方法ごとに適切な関数を呼び出すメイン関数
def calculate_fee(delivery, payment):
if delivery == 'standard': # 【6】標準配送の場合
return standard_fee(payment)
elif delivery == 'express': # 【7】速達配送の場合
return express_fee(payment)
else:
return 1200 # 【8】配送方法が不明の場合
# 【9】テスト実行
print(calculate_fee('standard', 'credit')) # 【10】500が出力される
関数分割によって、
・ ネストが浅くなり、見通しがいい設計になった
・ 各支払い条件のロジックが関数に分離されており、保守性が高くなった
という改善点を得た
【C】 クラスによる分離(拡張性の高い設計)
# 【1】配送手数料計算の基底クラス(インターフェース)
class FeeStrategy:
def calculate(self, payment):
raise NotImplementedError("サブクラスで実装する必要がある")
# 【2】標準配送用のクラス
class StandardFee(FeeStrategy):
def calculate(self, payment):
if payment == 'credit':
return 500
elif payment == 'cash':
return 600
else:
return 700
# 【3】速達配送用のクラス
class ExpressFee(FeeStrategy):
def calculate(self, payment):
if payment == 'credit':
return 800
elif payment == 'cash':
return 900
else:
return 1000
# 【4】配送方法に応じて適切な戦略を返すファクトリ関数
def get_fee_strategy(delivery):
if delivery == 'standard': # 【5】標準配送の場合
return StandardFee()
elif delivery == 'express': # 【6】速達配送の場合
return ExpressFee()
else:
return None # 【7】配送方法が不明の場合
# 【8】メイン処理
def calculate_fee(delivery, payment):
strategy = get_fee_strategy(delivery) # 【9】適切な戦略オブジェクトを取得
if strategy is None: # 【10】不明な配送方法の場合
return 1200
return strategy.calculate(payment) # 【11】戦略オブジェクトのcalculateを呼び出す
# 【12】テスト実行
print(calculate_fee('standard', 'credit')) # 【13】500が出力される
# 実行結果
500
上記をまとめると以下
設計方法 | 特徴 | 利点 | 欠点 |
---|---|---|---|
関数分割 | 各処理を関数で分離 | 実装が容易、可読性向上 | 条件が増えると関数が増える |
クラス設計 | 各処理をクラスで分離、ポリモーフィズム活用 | 拡張が容易、戦略パターンに適応可能 | 小規模では冗長になりがち |
Q.8-4
複雑なネストの中で break
や continue
を使うときの注意点を、while
との組み合わせで述べよ。
問題背景
(ネストに関する問題なので、このタイミングで扱うことにしました)
【1】 while
ループ
while
ループは、「条件が True
の間、処理を繰り返す」ための制御構文。条件が False
となった瞬間にループを終了する。
while 条件:
処理
while True:
で無限ループを作ることも可能。
【2】 break
文
break
文は現在のループ(最も内側のループ)を途中で強制終了し、ループの外側に制御を移す。
while True:
if 条件:
break # この while を抜ける
【3】 continue
文
continue
文は現在のループのその回の処理を中断し、次の繰り返しに移る。
while True:
value = input()
if value == 'skip':
continue # 残りの処理を飛ばして次のループへ
print("入力値:", value)
【4】 ネストさたループと break
, continue
の注意点
多重ループにおいて、break
, continue
が作用するのは最も内側のループのみ。すなわち外側のループを終了したい場合、内側の break
だけでは止まらない。
外側のループまで終了させるには、フラグ変数の利用/関数分割/ return
を使わなければならない。
while True: # 外側ループ
while True: # 内側ループ
if 条件:
break # → 内側しか抜けない!
解答例とコードによる実践
【A】 間違った設計例
while True: # 外側の無限ループ
while True: # 内側の無限ループ
user_input = input("終了しますか? (y/n): ")
if user_input == 'y':
break # これは内側のループしか抜けない
print("内側のループを抜けました")
このコードでは、break
は内側の while
しか終了しないため、外側の while True
は止まらない。
【B】 フラグ変数を用いた改善コード
# 【1】外側のループを制御するためのフラグを用意する
running = True # 外側の while を制御するフラグ
# 【2】外側の無限ループ(メインループ)
while running:
print("メインメニュー") # 【3】ユーザーにメインメニューを表示する
# 【4】内側の無限ループ(サブメニューなど)
while True:
# 【5】ユーザーに終了するか尋ねる
user_input = input("終了しますか? (y/n): ")
if user_input == 'y': # 【6】終了したい場合
running = False # 【7】外側のループを止めるためにフラグを False にする
break # 【8】内側ループを抜ける
elif user_input == 'n': # 【9】終了しない場合
print("続行します") # 【10】続行メッセージを表示
break # 【11】内側ループだけを抜けてメインメニューに戻る
else: # 【12】誤った入力の場合
print("y か n を入力してください") # 【13】再入力を促す
# 【14】すべてのループを終了した後の処理
print("プログラムを終了しました")
# 実行結果
メインメニュー
終了しますか? (y/n): n
続行します
メインメニュー
終了しますか? (y/n): x
y か n を入力してください
終了しますか? (y/n): y
プログラムを終了しました
【C】 関数分割により内外のループを整理
# 【1】内側のループの処理を関数として定義する
def sub_menu():
# 【2】内側の無限ループ
while True:
user_input = input("終了しますか? (y/n): ") # 【3】ユーザー入力を取得する
if user_input == 'y': # 【4】終了する場合
return True # 【5】True を返して終了したい意思を伝える
elif user_input == 'n': # 【6】続行する場合
print("続行します") # 【7】続行メッセージ
return False # 【8】False を返して終了しない意思を伝える
else:
print("y か n を入力してください") # 【9】無効な入力への対応
# 【10】メイン処理
def main():
while True: # 【11】外側の無限ループ(メインメニュー)
print("メインメニュー") # 【12】メインメニューの表示
if sub_menu(): # 【13】sub_menu から True が返ってきた場合
break # 【14】外側のループも終了
print("プログラムを終了しました") # 【15】ループ終了後の処理
# 【16】メイン関数を呼び出して実行
main()
メインメニュー
終了しますか? (y/n): n
続行します
メインメニュー
終了しますか? (y/n): x
y か n を入力してください
終了しますか? (y/n): y
プログラムを終了しました
【D】 return
を用いて早期リターンを行う
# 【1】メイン処理を関数化
def main():
while True: # 【2】外側の無限ループ(メインメニュー)
print("メインメニュー") # 【3】メインメニューの表示
while True: # 【4】内側の無限ループ
user_input = input("終了しますか? (y/n): ") # 【5】ユーザー入力を取得する
if user_input == 'y': # 【6】終了する場合
print("プログラムを終了しました") # 【7】終了メッセージ
return # 【8】関数全体から即リターン → ループ両方終了
elif user_input == 'n': # 【9】続行する場合
print("続行します") # 【10】続行メッセージ
break # 【11】内側ループだけ抜ける
else:
print("y か n を入力してください") # 【12】無効な入力への対応
# 【13】メイン関数を呼び出して実行
main()
メインメニュー
終了しますか? (y/n): n
続行します
メインメニュー
終了しますか? (y/n): x
y か n を入力してください
終了しますか? (y/n): y
プログラムを終了しました
本問をまとめると以下。
手法 | 特徴 | 利点 | 欠点 |
---|---|---|---|
フラグ変数を使う | 条件判定用の変数でループ継続・終了を制御する | 状態を明示でき、複数条件を柔軟に制御できる | フラグ管理が複雑になると可読性が落ちる |
関数を分ける | 外側と内側を関数で役割分担する | 責任が分離され、再利用しやすい | 小規模ならやや冗長になりがち |
return を使う | 早期リターンで処理を即終了する | シンプルで読みやすい | 大規模になると流れがわかりにくくなることがある |
Q.8-4
複数の if
を入れ子にして list.append()
を呼ぶ処理を、リスト内包表記に変換できる条件を分析せよ。また、入れ子の if
を関数の条件引数および lambda 式で制御する方法を、呼び出し時の柔軟性の観点から評価せよ。
問題背景
【1】list.append()
によるリスト生成
リストに要素を追加する標準的な方法は、list.append()
である。
result = []
for x in data:
if 条件:
result.append(x)
【2】リスト内包表記(List Comprehension)
条件に合う要素を1行でリストに追加できる記法。
result = [x for x in data if 条件]
複数の if
が単なるフィルター(判定条件)であり、順序が重要でない場合は and
で結合してリスト内包表記に変換可能。
【3】関数引数による条件制御
関数に条件を渡すことで、if
の切り替えを関数外から制御できる。
def should_append(x, check_even=False):
if check_even and x % 2 != 0:
return False
return True
【4】lambda
式
1行で書ける関数。条件関数を lambda
で柔軟に指定できる。
filter_func = lambda x: x > 2 and x % 2 == 0
result = [x for x in data if filter_func(x)]
解答例とコードによる実践
【A】ネストのif
+ .append()
(悪い例)
# 【1】結果を入れるリスト
result = []
# 【2】0〜9 までループ
for x in range(10):
# 【3】条件1:3より大きい
if x > 2:
# 【4】条件2:偶数
if x % 2 == 0:
# 【5】条件を満たしたものを追加
result.append(x)
# 【6】結果の表示
print(result)
交換可能不可能については以下
条件 | 変換可能か | 理由 |
---|---|---|
単なる判定(フィルター) | 可能 |
and で結合できる |
break , continue , else を含む |
不可 | 内包表記には制御構文が書けない |
【B】リスト内包表記への変換
# 【1】リスト内包表記でフィルター条件を and で結合
result = [x for x in range(10) if x > 2 and x % 2 == 0]
# 【2】結果の表示
print(result) # 【3】[4, 6, 8] が出力される
【C】関数引数で制御する設計(条件切り替えが可能)
# 【1】条件を引数で制御する関数
def should_append(x, check_gt_2=False, check_even=False):
# 【2】3より大きいかをチェック(必要な場合)
if check_gt_2 and x <= 2:
return False
# 【3】偶数かをチェック(必要な場合)
if check_even and x % 2 != 0:
return False
return True # 【4】条件を満たす場合 True
# 【5】結果を入れるリスト
result = []
# 【6】ループ処理
for x in range(10):
# 【7】should_append 関数で条件判定
if should_append(x, check_gt_2=True, check_even=True):
result.append(x)
# 【8】結果の表示
print(result)
【D】lambda
式による柔軟な制御(呼び出し時の条件変更が容易)
# 【1】条件を lambda で定義
filter_func = lambda x: x > 2 and x % 2 == 0
# 【2】リスト内包表記で filter_func を利用
result = [x for x in range(10) if filter_func(x)]
# 【3】結果の表示
print(result)
# 実行結果
[4, 6, 8]
本問題をまとめると、呼び出し時の柔軟性評価については以下。
手法 | 柔軟性 | 説明 |
---|---|---|
入れ子 if + append
|
低い | 条件を変更するにはコードを直接書き換える必要がある |
リスト内包表記 | 中程度 | 簡潔だが条件を変えるには式の修正が必要 |
関数引数 + フラグ制御 | 高い | 条件追加・変更が簡単で、関数再利用が可能 |
lambda 式による関数渡し |
非常に高い | 呼び出し時に動的に条件を切り替えられる |
【補足】 複合条件とデフォルト関数の組み合わせ
複雑な条件(複数条件を必要に応じて切り替えるなど)をリストフィルターとして用いたい場合、関数を引数として渡すと柔軟に設計できる。
更に、デフォルト引数として条件関数(lambda
や def
で定義した関数)を指定することで、呼び出し側が何も指定しなくても動作し、必要に応じて条件のみを差し替えられる。
文法背景
【1】Pythonでは関数もオブジェクト
def process(data, condition=lambda x: True):
return [x for x in data if condition(x)]
上記で condition
は、「引数を1つとり、True
/ False
」である、lambda
でその場で定義することも可能。
もう少し具体的な例であれば、
# 条件:3より大きく、かつ偶数
condition = lambda x: x > 3 and x % 2 == 0
process(range(10), condition)
# 出力: [4, 6, 8]
コードによる実践
【A】コード例.1
# 【1】条件関数を引数に持ち、デフォルトで「すべて通す」フィルター
def process(data, condition=lambda x: True):
# 【2】内包表記で条件を満たすものだけを返す
return [x for x in data if condition(x)]
# 【3】デフォルト(条件なし)の場合
print(process(range(5))) # 【4】[0, 1, 2, 3, 4]
# 【5】条件を指定(3より大きく、偶数)
my_condition = lambda x: x > 3 and x % 2 == 0
# 【6】条件ありで呼び出し
print(process(range(10), my_condition)) # 【7】[4, 6, 8]
【B】別コード例(標準関数 + 自作関数)
def is_even(x):
return x % 2 == 0
def greater_than_three(x):
return x > 3
# 複合条件を組み合わせる関数
def combined_condition(x):
return is_even(x) and greater_than_three(x)
print(process(range(10), combined_condition)) # [4, 6, 8]
複雑な条件を関数に分けておくことで、可読性と拡張性が向上する。
あとがき
今回も「if
による条件分岐」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話を中心に選んでみました。次回からは、「for
/ while
によるループ処理」を扱います。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)