演習形式で学ぶPythonプログラミング ～基本文法編～vol.2

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

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

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

今回は「算術演算子」について扱います。それでは始めましょう！(また今回から、解説文も「である」調で記述します。)

# Q.2-1
#### `+` 演算子が int、float、str、list において異なる意味を持つ理由を、内部メソッドの観点から比較せよ。

### 問題背景：演算子オーバーロードと`__add__()`メソッド
Pythonでは、`+` 演算子は 内部的に`__add__()`メソッドを呼び出す構文糖衣(シンタックスシュガー)である。
つまり、次の2つは同じ意味である：

```python
a + b
a.__add__(b)
```
データ型ごとにこの `__add__()`の実装が異なるため、 `+`の動作も変わる。これは「演算子オーバーロード」と呼ばれる仕組みである。
各型における `+` の意味と `__add__()` の実装意図は以下。
| 型     | `+` の意味         | 背後で呼ばれるメソッド | 実装の意味合い                        |
|--------|--------------------|--------------------------|----------------------------------------|
| int    | 数値の加算         | `int.__add__()`          | 数学的な加算                           |
| float  | 実数の加算         | `float.__add__()`        | 浮動小数点数の加算                     |
| str    | 文字列の連結       | `str.__add__()`          | 文字列の末尾結合                       |
| list   | リストの結合       | `list.__add__()`         | 新しいリストを返す（要素の連結）      |

### 解答例(コメントアウト付き)
```python
# 【1】int 型：数学的な加算を行う
a = 1
b = 2
print(f"a + b = {a + b}")  # 【2】→ 3（int.__add__）

# 【3】float 型：浮動小数点加算を行う
c = 1.5
d = 2.5
print(f"c + d = {c + d}")  # 【4】→ 4.0（float.__add__）

# 【5】str 型：文字列の連結を行う
s1 = 'Hello, '
s2 = 'World!'
print(f"s1 + s2 = {s1 + s2}")  # 【6】→ 'Hello, World!'（str.__add__）

# 【7】list 型：リストの要素結合を行う
l1 = [1, 2]
l2 = [3, 4]
print(f"l1 + l2 = {l1 + l2}")  # 【8】→ [1, 2, 3, 4]（list.__add__）

# 【9】内部メソッドを直接呼び出して確認
print(f"a.__add__(b) = {a.__add__(b)}")  # 【10】→ 3
print(f"s1.__add__(s2) = {s1.__add__(s2)}")  # 【11】→ 'Hello, World!'
print(f"l1.__add__(l2) = {l1.__add__(l2)}")  # 【12】→ [1, 2, 3, 4]
```
```python
# 出力例
a + b = 3
c + d = 4.0
s1 + s2 = Hello, World!
l1 + l2 = [1, 2, 3, 4]
a.__add__(b) = 3
s1.__add__(s2) = Hello, World!
l1.__add__(l2) = [1, 2, 3, 4]
```

解答例としては、上記のコードを示しながら、
Pythonの `+` 演算子は型に応じて適切な `__add__()` が呼ばれる仕組みになっており、このような柔軟な仕組みから、数値では加算、文字列では連結、リストでは結合といった自然な使い分けが可能である。ユーザー定義クラスでも `__add__()` を実装することで、独自の加算ルールを定義できる。
のように書けばよい。

# Q.2-2
#### `True` + `True` + `False` が `2` になる理由を、`bool` と `int` の継承関係から説明せよ。

### 問題背景：`bool`と`int`の継承関係
Pythonでは、`bool`は`int`のサブクラス(派生クラス)であり、`True`, `False`はそれぞれ`1`, `0`に相当する整数値として振舞う。これは以下のように確認可能。

```python
issubclass(bool, int)  # → True

int(True)   # → 1
int(False)  # → 0
```

このような背景もあり、
・ `True + True + False` は、内部的には `1 + 1 + 0` と同じように計算される
・ `+` 演算子は `int.__add__()` を呼び出すので、`bool` はそのまま `int` として加算される
わけである。
例えばこれを利用して、真理値の合計は「条件を満たす個数のカウント」に用いることが出来る。

```python
# 条件に合致する数をカウントする例
data = [5, 8, 2, 10, 3]
count = sum(x > 5 for x in data)  # True→1, False→0 が加算される
print(f"5より大きい要素の数: {count}")  # → 2
```

### 解答例(コメントアウト付き)
```python
# 【1】True + True + False を評価して出力（整数と同様に扱われる）
print(f"True + True + False = {True + True + False}")  # → 2

# 【2】bool の継承関係を確認（bool は int のサブクラス）
print(f"bool は int のサブクラスか？ → {issubclass(bool, int)}")  # → True

# 【3】True, False の整数値としての値を確認
print(f"int(True) = {int(True)}")    # → 1
print(f"int(False) = {int(False)}")  # → 0

# 【4】型の確認：bool 型だが int として振る舞う
print(f"type(True) = {type(True)}")   # → <class 'bool'>
```
```python
# 出力例
True + True + False = 2
bool は int のサブクラスか？ → True
int(True) = 1
int(False) = 0
type(True) = <class 'bool'>
```

解答例としては、
`bool`は`int`を継承しているサブクラスであり、`True == 1`, `False == 0`として振る舞う。`+` 演算子は、`init.__add__()`を用いるため、`bool`も`int` のように加算されるため、問題のような振る舞いをする。
ということを、コードを示しながら書けばいいかと思います。


# Q.2-3
#### `None + 1` と `None is None` の挙動を対比し、`None` の型的・意味的役割を述べよ。

### 問題背景：`None` の型と意味
#### [1] `None`の型的役割
・ `None`はPythonにおける「何もない」ことを表す唯一の値
・ 型は `NoneType` (この型には`None`という値しか存在しない)
・ `None`は演算に参加できないため、文字列や数値との加算は不可能(`TypeError`)

#### [2] `None`の意味的役割
・ 値が未設定で、初期化されていない状態
・ 戻り値を持たない関数が暗黙的に返す値
・ 条件文や引数のデフォルト値としてもよく用いられる

この結果、`None + 1` と `None is None` は、以下のような挙動を示す。

| 式            | 結果       | 説明                                      |
|---------------|------------|-------------------------------------------|
| None + 1      | TypeError  | None は数値ではないため演算できない       |
| None is None  | True       | None はシングルトンであり同一性が成立する |

### 解答例(コメントアウト付き)
```python
# 【1】None の型を確認する（NoneType）
print(f"type(None) = {type(None)}")  # → <class 'NoneType'>

# 【2】None + 1 の演算を試みる（例外が発生する）
try:
    result = None + 1  # 【3】加算はできない → TypeError
except TypeError as e:
    print(f"None + 1 はエラー: {e}")  # 【4】エラーメッセージを表示

# 【5】None 同士の同一性判定（is）
print(f"None is None → {None is None}")  # 【6】True（同一オブジェクト）

# 【7】None を戻り値として返す関数
def do_nothing():
    pass  # 明示的な return がないと None を返す

# 【8】関数の戻り値を確認
ret = do_nothing()
print(f"do_nothing() の戻り値 → {ret}")        # 【9】None
print(f"戻り値は None か？ → {ret is None}")   # 【10】True
```
```python
# 出力例
type(None) = <class 'NoneType'>
None + 1 はエラー: unsupported operand type(s) for +: 'NoneType' and 'int'
None is None → True
do_nothing() の戻り値 → None
戻り値は None か？ → True
```
解答については、上記をまとめるとよい。

# Q.2-4
####  `id()` 関数を使って、`int`, `float`, `str` 型のオブジェクト再利用（interning）挙動を確認せよ。

### 問題背景：オブジェクトのIDとインターニング
##### オブジェクトの再利用(interning)
・ Python では、小さい整数や短い文字列に関して、メモリ効率のために同じオブジェクトを再利用する(interning)ことがある。例えば、`-5` ~ `256`の間の数値や、一部の短い文字列は再利用される。
➡ 整数の場合は、`-5` ~ `256`の間の整数をキャッシュして再利用し、`"abc"` のような短い文字列は再利用されることが多いが、長い文字列や動的に生成された文字列は通常再利用されない。

### 解答例(コメントアウト付き)
```python
# 【1】整数の例：int 型のインターニング挙動
x = 256  # 【2】256 はインターニングされている
y = 256  # 【3】再利用される
print(f"x と y の id は同じか？ {id(x) == id(y)}")  # → True（同じオブジェクト）

# 【4】256 を超える整数で再利用されない例
a = 257  # 【5】257 はインターニングされていない
b = 257  # 【6】新たなオブジェクトが生成される
print(f"a と b の id は同じか？ {id(a) == id(b)}")  # → False（異なるオブジェクト）

# 【7】浮動小数点数型の例：float 型は通常インターニングされない
f1 = 1.5
f2 = 1.5
print(f"f1 と f2 の id は同じか？ {id(f1) == id(f2)}")  # → False（通常 float は再利用されない）

# 【8】文字列の例：短い文字列はインターニングされる
str1 = "hello"
str2 = "hello"
print(f"str1 と str2 の id は同じか？ {id(str1) == id(str2)}")  # → True（同じオブジェクト）

# 【9】長い文字列の例：長い文字列はインターニングされない
long_str1 = "a" * 1000  # 【10】長い文字列は通常インターニングされない
long_str2 = "a" * 1000
print(f"long_str1 と long_str2 の id は同じか？ {id(long_str1) == id(long_str2)}")  # → False（異なるオブジェクト）
```
```
# 出力例
x と y の id は同じか？ True
a と b の id は同じか？ False
f1 と f2 の id は同じか？ False
str1 と str2 の id は同じか？ True
long_str1 と long_str2 の id は同じか？ False
```


# あとがき
今回は「算術演算子」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話を中心に選んでみました

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