演習形式で学ぶPythonプログラミング vol.6 ~ ifによる条件分岐①~

# まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。

対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。

2000問ほどあるストックを問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。

今回は「`if` による条件分岐」について扱います。それでは始めましょう！

# Q.6-1
##### `if func():` のような文において、`func()` が `print("hello")` を返す関数だった場合の分岐挙動を確認せよ。

### 問題背景
#### 【1】`if` 文の条件式と真偽値判定(true / false)
Pythonの `if` 文では、条件式が `True`ならifブロック、`False` ならelseブロックを実行する。
`if 条件：` の条件では、`False`, `None`, `0`, `0.0`, `''(空文字列)`, `[]`, `()`, `{}`, `その他の空オブジェクト` は `False` と判定される。
→ それ以外は `True` とみなされる。
#### 【2】 `print()` 関数の戻り値
`print()` は出力するための関数で、戻り値(return value)は `None`。

関数を呼び出すと、計算結果を返す(returnする)場合がある。例えば、
```python
def add(a, b):
    return a + b  # a + b の計算結果を返す

result = add(3, 4)  # ここで result には 7 が入る
print(result)       # → 7 と表示される
```
一方で、計算結果を返すのではなく、何かの「動作」をすることを副作用(side effect)と呼ぶ。`print()` は、典型的な「副作用」を持つ関数。
```python
result = print("hello")    # hello
print(result)    # None
```
上記であれば、`hello` と表示されるが、`print()` 事態は何も返していない( 'None' を返している) = 「表示する」という動作だけ行い、戻り値は `None` になっている。

#### [3] 本問のポイント
```python
if func():
```
の `func()` が `print("hello")` を返すとすると、実際には `print('hello')` の戻り値(`None`) が　`if` の条件に使われる。
→ `None` は `False` 扱いなので、 `if` ブロックに入らない。

### 解答例とコードによる実践
```python
# 【1】関数funcを定義する
def func():  # 関数funcを作成
    print("hello")  # 【2】helloと表示する
    # 【3】print() は None を返す（これが if で使われる戻り値になる）

# 【4】if 文で func() の戻り値を判定する
if func():  # 【5】func() を呼び出し、「hello」と表示するが戻り値は None（False 扱い）
    print("Entered IF block")  # 【6】もし True ならこちらが実行される（今回は実行されない）
else:
    print("Entered ELSE block")  # 【7】func() の戻り値が None なのでこちらが実行される
```
```
# 出力結果
hello
Entered ELSE block
```

### 本問のポイント
| 誤解しやすい点             | 正しい理解                                      |
|----------------------------|-------------------------------------------------|
| `print()` が何かを「返す」 | 返り値は `None` で、`if` 判定はこれを見る        |
| 画面に何か出たら `True`？  | 出力と戻り値は無関係                            |
| `if print("hello"):`       | 常に `False` 扱い（`None` を返すため）         |


# Q.6-2
##### if 文の中で list.append() のような副作用を持つ関数を評価式に含めると、処理順序が混乱する理由を示せ。

### 問題背景
#### [1] 副作用(side effect)
関数やメソッドが、戻り値を返すだけでなく外部状態を変更する動作を行うこと
例えば、`list.append()` → 戻り値は `None`だが、リスト本体を書き換える(副作用)

#### [2] `if` 文は「戻り値」で判定する
`if condition:` の `condition` には、戻り値が `True` であればifブロックに入り、`False` であればelseブロックに入る。(副作用があるかは判定と無関係)

#### [3] 混乱する理由
副作用を「評価式」に含めると、
・ 判定と動作(副作用)が混ざる
・ 「判定のための式」が「データを書き換える式」にもなってしまい、コードの意図が分かりづらい
・ 戻り値と副作用を分けないと、処理順序が曖昧になる

### 解答例とコードによる実践
```python
# 【1】空のリストを用意する
lst = []  # 判定に使うリストを作成（まだ何も入っていない）

# 【2】if 文の中で append を評価式に含めてみる（問題のある書き方）
if lst.append(1):  # 【3】append はリストに1を追加するが、戻り値は None なので False 扱い
    print("Entered IF block")  # 【4】ここは実行されない（None は False 扱いだから）
else:
    print("Entered ELSE block")  # 【5】こちらが実行される

# 【6】リストの内容を確認する
print("List contents:", lst)  # 【7】副作用としてリストには [1] が追加されている
```
```python
# 出力結果
Entered ELSE block
List contents: [1]
```

上記のコードは以下のように、副作用と判定を分けるべきである。
```python
lst = []
lst.append(1)  # 副作用は明示的に行う
if lst:        # 判定はリストが空かどうかだけにする（正しい）
    print("List is not empty")
else:
    print("List is empty")
```
→ 副作用と判定を明確に分ければ、処理順序がはっきりする。

# Q.6-3
##### `if x in range(0, 10):` のような条件式と、`0 <= x < 10` との違いを評価の効率と構文の可読性から比較せよ。

### 問題背景
#### 【1】条件式の2つの書き方
| 書き方                  | 説明                                 |
|-------------------------|--------------------------------------|
| `if x in range(0, 10):`  | `range(0, 10)` の中に `x` があるか判定 |
| `if 0 <= x < 10:`        | `x` が 0 以上 10 未満か判定           |

#### 【2】評価の効率（時間計算量）

| 方法                 | 計算量（時間）                                   |
|----------------------|--------------------------------------------------|
| `x in range(0, 10)`   | O(1)（`range` は整数シーケンスなので即判定）    |
| `0 <= x < 10`         | O(1)（数値比較2回）                             |

#### 【3】構文の可読性

| 書き方              | 読みやすさ（一般的な評価）                      |
|---------------------|--------------------------------------------------|
| `x in range(0, 10)` | 「範囲内か？」という意味が直感的                |
| `0 <= x < 10`       | 数学的な範囲指定と一致、簡潔                    |

上記をまとめると、
| 観点       | `x in range(0, 10)`                  | `0 <= x < 10`                          |
|------------|--------------------------------------|----------------------------------------|
| 効率       | O(1)                                | O(1)                                  |
| 可読性     | 範囲指定として直感的                | 数学的で簡潔                          |
| 使い分け   | 範囲が動的なとき便利                | 数式に慣れている場合はこちら          |
| Python的   | 両方許容される（好みで選択可）      | 両方許容される（好みで選択可）        |

### 解答例とコードによる実践

```python
# 【1】x の値を設定する（範囲の中の例）
x = 5  # 判定したい数値を準備

# 【2】range を使った範囲判定
if x in range(0, 10):  # 【3】range(0, 10) の中に x があるかどうかを判定（0以上10未満）
    print("x is in range (using 'in range')")  # 【4】範囲内ならこちらを表示
else:
    print("x is NOT in range (using 'in range')")  # 【5】範囲外ならこちら

# 【6】比較演算子を使った範囲判定
if 0 <= x < 10:  # 【7】0以上かつ10未満かを比較演算子で判定（2つの比較を同時に行う）
    print("x is in range (using 'comparison operators')")  # 【8】範囲内ならこちらを表示
else:
    print("x is NOT in range (using 'comparison operators')")  # 【9】範囲外ならこちら
```
```python
x is in range (using 'in range')
x is in range (using 'comparison operators')
```

# Q.6-4
##### `if some_dict.get("key"):` のような記述が `None` や `0` に対して意図と異なる評価をする例をコードで示せ。

### 問題背景
#### 【1】`dict.get(key)` の動作
`dict.get("key")` は、指定したキーが存在すればその値を返す。`key` が存在しない場合は `None` を返す(デフォルト動作)。

#### 【2】問題となるケース。
`if` 文は条件式の「真偽値」で判定を行うため、 `some_dict.get("key")` が `0` や `''`の場合も、`False` と判定される。結果、`key` の存在を判定したかったのに、値が0のため存在しないと誤判定されるというバグにつながる。

即ち、`if some_dict.get("key"):` の書き方は、「`key` が存在し、かつ値が `True`」でなければ、条件式として `True` にならない。

### 解答例とコードによる実践
```python
# 【1】辞書を定義する（値が 0 のキーを含む）
some_dict = {"count": 0}  # "count" というキーは存在するが、値は 0

# 【2】if some_dict.get("count") で判定する（誤った書き方）
if some_dict.get("count"):  # 【3】get() の戻り値は 0 → これは False 扱いされる
    print("Key exists and has a truthy value (wrong behavior)")  # 【4】ここは実行されない
else:
    print("Key does not exist or value is falsy (unexpected)")  # 【5】こちらが実行される（誤判定）

# 【6】正しい書き方：キーの存在だけを確認する
if "count" in some_dict:  # 【7】キーの存在を判定。値が 0 でも関係ない
    print("Key exists (correct behavior)")  # 【8】こちらが正しく実行される
else:
    print("Key does not exist (unexpected)")
```
```python
Key does not exist or value is falsy (unexpected)
Key exists (correct behavior)
```

本文をまとめると、

| 書き方                     | 動作                             | 問題点                                      |
|----------------------------|----------------------------------|---------------------------------------------|
| `if some_dict.get("key"):`  | 値が `None`, `0`, `''` だと `False` | 値が `0` の場合も「キーがない」と誤判定する  |
| `if "key" in some_dict:`    | キーの存在だけ判定               | 正しく「存在しているか」をチェックできる    |


# Q.6-5
##### `if` 文の中に `try-except` を含める構文と、`try` 文の中に `if` を入れる構文の意味的・構文的違いを例示せよ。

### 問題背景
#### 【1】`if` 文と `try-expect` 文の役割

| 構文           | 目的                                               |
|----------------|----------------------------------------------------|
| `if`           | 条件式が `True` のときのみ処理を実行する（条件分岐） |
| `try-except`   | エラー（例外）が発生する可能性のある処理を安全に実行する（例外処理） |

#### 【2】`if` 文の中に `try-except` がある場合
条件を先に判定し、条件が `True` ならば例外が出るかもしれない処理を試す。例えば、ファイルが存在するときだけ開く → ただし開くときに失敗するかもしれない
```python
if 条件:
    try:
        # エラーが出るかもしれない処理
    except:
        # エラーへの対応
```

#### 【3】`try` の中に `if` がある場合
先に例外が出るかどうかを確認し、成功したら条件判定を行う。例えば、ファイルを開いてから、その内容によって分岐する。

```python
try:
    # エラーが出るかもしれない処理
    if 条件:
        # 条件が True のときの処理
except:
    # エラーへの対応
```

上記をまとめると、「条件を先に見るのか」「エラーを先に見るのか」で意味が異なる。特に、I/O処理や計算途中でエラーが出る場合、設計ミスにつながる。

### 解答例とコード例
#### 【A】 `if` 文と `try-expect` 文の役割
```python
# 【1】ファイル名を指定する
filename = "sample.txt"  # 読み込みたいファイル名

# 【2】ファイルが存在するか条件判定
import os  # 【3】ファイル存在確認用に os モジュールを使う
if os.path.exists(filename):  # 【4】ファイルが存在する場合だけ読み込もうとする
    try:
        with open(filename, 'r') as f:  # 【5】ファイルを開く（エラーが出るかもしれない）
            print(f.read())  # 【6】ファイル内容を表示
    except IOError:  # 【7】ファイルが開けなかった場合の処理
        print("Could not open the file.")  # 【8】エラーメッセージを表示
else:
    print("File does not exist.")  # 【9】ファイルが存在しない場合はこちらを表示
```

#### 【B】`try` の中に `if` がある場合
```python
# 【1】ファイル名を指定する
filename = "sample.txt"  # 読み込みたいファイル名

# 【2】例外が発生するかどうかを先にチェック
try:
    with open(filename, 'r') as f:  # 【3】まずファイルを開こうとする（エラーが出るかもしれない）
        content = f.read()  # 【4】ファイル内容を取得する
        # 【5】読み込んだ内容に応じて条件分岐する
        if "important" in content:  # 【6】内容に "important" が含まれていれば
            print("Important information found!")  # 【7】その場合の処理
        else:
            print("No important information.")  # 【8】含まれていない場合
except IOError:  # 【9】ファイルが開けなかった場合の処理
    print("Could not open the file.")  # 【10】エラーメッセージを表示
```
```python
# 出力結果
File does not exist.
Could not open the file.
```

本問をまとめると以下。

| 書き方                  | 先に確認するもの              | 例外処理のタイミング                           | 使いどころ                                              |
|-------------------------|------------------------------|------------------------------------------------|---------------------------------------------------------|
| if の中に try-except     | 条件（存在チェックなど）      | 条件が `True` のときだけ例外を考慮する           | 条件が成り立たないなら例外処理自体不要なとき            |
| try の中に if           | 例外（エラー発生の有無）      | まず例外を考慮し、その後条件で処理を分岐         | 例外が出る可能性が常にある処理を安全に行いたいとき     |

# Q.6-6
##### `if x and not y:` と `if not y and x:` の処理順序が異なる場合に、`y` に副作用があるときの挙動を比較せよ。

### 問題背景
#### 【1】Pythonの `and` 演算子(論理積)
左から順に評価される(短絡評価；short-circuit evaluation)、左側が `False` の時点で右側は評価されない。

#### 【2】 短絡評価の評価順序
| 条件式                | 評価順序          |
|------------------------|-------------------|
| `if x and not y:`      | ① `x` → ② `y`     |
| `if not y and x:`      | ① `y` → ② `x`     |

#### 【3】 副作用(side effect)
関数呼び出しや式の評価によって状態が変わること。例えば、`print()`, `pop()`, `append()` など。 

`y` に副作用がある場合、「`y` が評価されるかどうか」が挙動を左右する。例えば `y()` が副作用を持つ関数だとすると、`if x and not y():` では `x` が `False` なら `y()` が呼ばれない。
→ 評価順序と副作用発生の有無 が密接に関係している。

### 解答例とコードによる実践
```python
# 【1】副作用のある関数 y を定義する
def y():  # y は何か副作用を持つ関数（ここでは出力する）
    print("y() was called!")  # 【2】呼び出されたときに表示する
    return False  # 【3】戻り値は False にする

# 【4】x の値を決める
x = True  # 【5】x は True とする（条件を満たす）

# 【6】if x and not y: の場合（x が True なら y() が呼ばれる）
print("Check: if x and not y():")
if x and not y():  # 【7】まず x が評価され、True なので次に y() が呼ばれる
    print("Entered IF block")  # 【8】y() の結果が False なので not False → True → ここが実行される
else:
    print("Entered ELSE block")  # 【9】こちらは実行されない

print()  # 区切り

# 【10】if not y and x: の場合（y() が先に呼ばれる）
print("Check: if not y() and x:")
if not y() and x:  # 【11】先に y() が呼ばれ、戻り値 False → not False → True → 次に x を評価
    print("Entered IF block")  # 【12】両方 True なのでこちらが実行される
else:
    print("Entered ELSE block")  # 【13】こちらは実行されない
```
```python
# 実行結果
Check: if x and not y():
y() was called!
Entered IF block

Check: if not y() and x:
y() was called!
Entered IF block
```

# Q.6-7
##### `if` 文に `lambda` 関数を使って動的な条件を構成する実装を作成し、その柔軟性とリスクを評価せよ。

### 問題背景
### [1] `if` 文における動的条件の意味
条件を「関数」として保持することで、条件を柔軟に変更できる。すなわち使いたいときに条件関数を選んで評価することが可能。これによって、複数の異なる条件を関数として管理できるため、コードがシンプルになる。一方で、関数オブジェクトを正しく呼び出さなかった場合は意図した挙動と異なる結果を出力したり、条件が複雑すぎると可読性も落ちる。

### 解答例とコードによる実践
```python
# 【1】データのリストを定義する
data = [5, -3, 0, 8, -1]  # 判定対象となる整数のリスト

# 【2】複数の条件関数を lambda で用意する
is_positive = lambda x: x > 0  # 【3】x が正のとき True
is_negative = lambda x: x < 0  # 【4】x が負のとき True
is_zero = lambda x: x == 0     # 【5】x が 0 のとき True

# 【6】ユーザーが選んだ条件（今回は動的に選ぶ代わりに手動で指定）
condition = is_positive  # 【7】ここを is_negative や is_zero に変えれば動作が変わる

# 【8】リストの各要素に対して条件関数を適用する
for num in data:  # 【9】リスト内の各数字に対してループ
    if condition(num):  # 【10】lambda 関数を呼び出して条件を判定（注意: 呼び出し忘れに注意, if conditon:では常にTrueになってしまう）
        print(f"{num} は条件を満たします")  # 【11】条件を満たす場合
    else:
        print(f"{num} は条件を満たしません")  # 【12】条件を満たさない場合
```

```python
# 出力結果
5 は条件を満たします
-3 は条件を満たしません
0 は条件を満たしません
8 は条件を満たします
-1 は条件を満たしません
```

また、条件関数を辞書で管理して、動的に選んで適用できる実装例を示す。

```python
# 【1】データのリストを定義する
data = [5, -3, 0, 8, -1]  # 判定対象の数値リスト

# 【2】判定条件を lambda 関数で定義し、辞書で管理する
conditions = {
    "positive": lambda x: x > 0,         # 【3】正の数を判定する関数
    "negative": lambda x: x < 0,         # 【4】負の数を判定する関数
    "zero": lambda x: x == 0             # 【5】ゼロかどうかを判定する関数
}

# 【6】どの条件を使うかを文字列で指定（動的に変えられる）
selected_condition_name = "negative"    # 【7】"positive", "negative", "zero" のいずれかを指定する

# 【8】辞書から選ばれた関数オブジェクトを取り出す
condition = conditions[selected_condition_name]  # 【9】辞書経由で条件関数を取得

# 【10】リストの各要素に対して、選択した条件関数を適用する
for num in data:  # 【11】リスト内の各数字に対してループ
    if condition(num):  # 【12】取得した関数を呼び出して条件判定（必ず呼び出す！）
        print(f"{num} は '{selected_condition_name}' の条件を満たします")  # 【13】条件を満たす場合
    else:
        print(f"{num} は '{selected_condition_name}' の条件を満たしません")  # 【14】満たさない場合
```
```python
5 は 'negative' の条件を満たしません
-3 は 'negative' の条件を満たします
0 は 'negative' の条件を満たしません
8 は 'negative' の条件を満たしません
-1 は 'negative' の条件を満たします
```

本問のポイントをまとめると以下の通り。

| 観点       | 内容                                                         |
|------------|--------------------------------------------------------------|
| 柔軟性     | 条件ごとに `lambda` で関数を準備すれば、条件切り替えが簡単    |
| コード管理 | 条件を一元管理できる（複数 `if-else` を排除できる）          |
| リスク     | 関数そのもの (`condition`) を `if` に書いてしまい、呼び出し忘れで常に `True` になる危険がある |
| 可読性     | 条件が増えると関数名の設計と管理が必要                        |
| 適用範囲   | 条件が動的に変わるフィルタリングや判定処理全般                 |


# あとがき
今回は「`if` による条件分岐」を扱いました。個人的には文法理解に資する6問を選んだつもりです。次回も「`if` による条件分岐」について扱います。

# 参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版（2024, Alex, O’Reilly Japan）
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう！プログラミング初心者向けの厳選100問を出題！(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)