演習形式で学ぶPythonプログラミング vol.11 ~ range 関数を用いたループ処理~

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

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

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

今回は「`range` 関数を用いたループ処理」について扱います。それでは始めましょう！

# Q.11-1
**`range(1, n + 1)` を使って `1` から `n` までの平方数をリストとして返す関数を定義し、`append()` を適切に使用せよ。なお、`n` は関数の引数とする。**

### 問題背景
#### 【1】 `range()` 関数
`range(start, stop)` は、`start` から `stop-1` までの整数の列を生成する。例えば、`range(1, n+1)` と書くことで、`1` から`n` までの整数を生成できる。

```python
range(1, 6)  # → 1, 2, 3, 4, 5
```

#### 【2】 `def` と `return`
`def` は関数を定義するために用いて、関数は入力を受け取って処理を行い、結果を返すことが出来る。`return` は、関数内で計算結果や処理結果を呼び出し元に返すためのキーワードである。

```python
# 【1】足し算を行う関数を定義
def add(a, b):
    # 【2】a と b を足して結果を返す
    result = a + b
    return result  # 【3】結果を返す

# 【4】関数を呼び出す（引数 3 と 4 を渡す）
sum_result = add(3, 4)  # 【5】戻り値は 7

# 【6】結果を表示
print(f"3 + 4 = {sum_result}")  # 【7】3 + 4 = 7 と表示される
```
```:実行結果
3 + 4 = 7
```
`def` を用いて関数を定義し、その中で特定の処理を行い、`return` を用いて処理結果を返す。

### 解答例とコードによる実践
```python
# 【1】平方数をリストとして返す関数を定義する
def generate_square_numbers(n):
    # 【2】結果を格納する空のリストを作成
    squares = []

    # 【3】1からnまでの整数をrangeで繰り返す
    for i in range(1, n + 1):
        # 【4】iの平方数を計算してリストに追加
        squares.append(i ** 2)  # 【5】平方数をリストに追加

    # 【6】最終的に平方数のリストを返す
    return squares

# 【7】関数を呼び出して、1から5までの平方数を取得
result = generate_square_numbers(5)

# 【8】結果を表示する
print(result)  # 【9】[1, 4, 9, 16, 25]
```
```:実行結果
[1, 4, 9, 16, 25]
```

# Q.11-2
**`range()` を使って九九の表(1〜9)を出力する二重ループを作成せよ。**
### 問題背景
#### 【1】 `range()` 関数
`range()` は指定した範囲の整数を生成する関数。特に繰り返し処理において便利。`range(start, stop)` は `start` から `stop-1` までの整数を生成し、`range(start, stop, step)` では、`step` の間隔で生成される。

```python
# 例：1から9までの数字を生成
for i in range(1, 10):
    print(i)
```

#### `for` 文による繰り返し処理
`for` 文は、イテラブル(リストやタプル、`range()` など)に対して、反復処理を行うために使用。2重の `for` 文を用いることで、2つのリストや範囲にわたる処理を同時に行うことが出来る。

```python
# 例：二重のfor文を使って1〜9の九九を計算
for i in range(1, 10):
    for j in range(1, 10):
        print(i * j, end="\t")  # 横に並べて表示
    print()  # 1行分が終わったら改行
```

### 解答例とコードによる実践
```python
# 【1】1から9までの整数を生成するためにrangeを使う
for i in range(1, 10):  # 【2】i は行（1から9まで）

    # 【3】i の各値に対して、1から9まで掛け算を繰り返す
    for j in range(1, 10):  # 【4】j は列（1から9まで）
        
        # 【5】掛け算の結果を表示
        print(f"{i * j:2}", end="\t")  # 【6】数字を2桁の幅で表示し、横に並べる
        
    # 【7】1行終わったら改行
    print()  # 【8】改行で次の行へ
```
```:実行結果
 2	 4	 6	 8	10	12	14	16	18	
 3	 6	 9	12	15	18	21	24	27	
 4	 8	12	16	20	24	28	32	36	
 5	10	15	20	25	30	35	40	45	
 6	12	18	24	30	36	42	48	54	
 7	14	21	28	35	42	49	56	63	
 8	16	24	32	40	48	56	64	72	
 9	18	27	36	45	54	63	72	81	
```

## 【補足】
### 【1】同様に、素数を判定しながら `n` 以下の素数を列挙するコードを記述すると以下。

```python
# 【1】素数判定を行う関数を定義する
def is_prime(num):
    # 【2】2以下の数は素数ではない（1は素数ではない）
    if num <= 1:
        return False
    # 【3】2は素数
    if num == 2:
        return True
    # 【4】偶数は2以外の素数ではない
    if num % 2 == 0:
        return False
    # 【5】3から √num までの数で割り切れるかをチェック
    for i in range(3, int(num**0.5) + 1, 2):  # 【6】奇数のみを調べる
        if num % i == 0:
            return False  # 【7】割り切れたら素数ではない
    # 【8】割り切れなかったら素数
    return True

# 【9】n 以下の素数をリストとして返す関数を定義
def prime_numbers_up_to_n(n):
    primes = []  # 【10】素数を格納する空のリスト
    # 【11】1 から n までの範囲をループして素数をチェック
    for num in range(2, n + 1):  # 【12】2 から n まで繰り返す
        if is_prime(num):  # 【13】num が素数かどうかをチェック
            primes.append(num)  # 【14】素数ならリストに追加
    # 【15】素数のリストを返す
    return primes

# 【16】n = 20 で試してみる
n = 20
result = prime_numbers_up_to_n(n)

# 【17】結果を表示
print(f"{n} 以下の素数: {result}")
```
```:実行結果
20 以下の素数: [2, 3, 5, 7, 11, 13, 17, 19]
```

# Q.11-3
**`range()` を動的に変更しながらループを行う構造を、`for` 文ではなく `while` を使ってシミュレートせよ。**

### 問題背景
#### `range()` と `while` の組み合わせ
`range()` を用いて動的に繰り返す場合、`while` を用いることで `range()` を途中で変更することが出来る。`for` 文ではイテラブルを事前に決めておくが、`while` ではループ内で条件を変更することで動的に変更した範囲を扱うことが出来る。

### 解答例とコードによる実践
初期値は1から始まり、途中で2倍のステップに変更する例を示す。
```python
# 【1】初期値と終了値を設定
start = 1  # 【2】開始値
end = 10   # 【3】終了値

# 【4】while ループを開始
while start <= end:  # 【5】start が end 以下の場合にループ
    print(start)  # 【6】現在の start の値を出力
    start += 1  # 【7】start を1増やす

    # 【8】途中で範囲を変更する条件（例：5を超えたら2倍にする）
    if start > 5:
        print("範囲を2倍に変更します")  # 【9】範囲変更のメッセージを表示
        end *= 2  # 【10】end を2倍に変更

# 【11】終了後のメッセージ
print("処理が完了しました")
```
```:実行結果
1
2
3
4
5
範囲を2倍に変更します
6
7
8
9
10
処理が完了しました
```

# Q.11-3
**`range()` を使って、カウントダウンタイマーを構築し、`1` 秒ごとに表示を更新するコードを作成せよ。また、日付や時刻を表現する構造を `datetime.timedelta` と組み合わせて作成せよ。**

### 問題背景
#### 【1】 `time.sleep()` 関数
`time.sleep()` は指定した秒数だけプログラムの実行を一時停止する関数。カウントダウンタイマーの実装において、1秒ごとに表示を更新するためにこの関数を使用する。

#### 【2】 `datetime` モジュール
`datetime` モジュールは、日付と時刻を扱うための標準ライブラリ。`datetime.timedelta` は日付や時間の差を表すために使用する。`timedelta(days=1)` は1日の差を表すオブジェクトを作成する。日付の加算や減算を行うために、`timedelta` クラスが使われる。`timedelta` は日数、秒数、マイクロ秒数を表現するためのクラスです。
```python
from datetime import timedelta

# 1日を表すtimedelta
one_day = timedelta(days=1)

# 日付に1日を加算
from datetime import date
today = date.today()
tomorrow = today + one_day
print(tomorrow)  # 明日の日付を出力
```

本文では、
カウントダウンタイマーの要素：
・ `range()` を使って、指定した時間から1秒ずつ減少させるカウントダウンを作成。
・ `time.sleep(1)` を使って、1秒ごとに表示を更新。
・ `datetime.timedelta` を使って、終了時間を分かりやすく表示することができる。
日付や時刻の表現：
・ `datetime.timedelta` を使って、開始時刻から残り時間を計算し、その差を表示することも可能。

### 解答例とコードによる実践
#### 【A】カウントダウンタイマーの作成
```python
import time
from datetime import timedelta

# 【1】カウントダウンタイマーの開始時間（秒数）
start_time = 10  # 【2】10秒のカウントダウンタイマー

# 【3】カウントダウンを行うループ
for remaining_time in range(start_time, 0, -1):  # 【4】10から1まで1秒ずつ減らす
    # 【5】残り時間を表示
    print(f"残り時間: {remaining_time}秒")
    
    # 【6】1秒待機してから次のループに進む
    time.sleep(1)  # 【7】1秒待機

# 【8】カウントダウン終了後のメッセージ
print("タイマー終了！")

# 【9】現在の日付と時刻の表示
current_time = timedelta(days=1)  # 【10】1日の差を表すtimedeltaを作成
print(f"現在の時間の差: {current_time}")
```
```:実行結果
残り時間: 10秒
残り時間: 9秒
残り時間: 8秒
残り時間: 7秒
残り時間: 6秒
残り時間: 5秒
残り時間: 4秒
残り時間: 3秒
残り時間: 2秒
残り時間: 1秒
タイマー終了！
現在の時間の差: 1 day, 0:00:00
```

#### 【B】 5日分の日付リストを表示
```python
from datetime import timedelta

# 1日を表すtimedelta
one_day = timedelta(days=1)

# 日付に1日を加算
from datetime import date
today = date.today()
tomorrow = today + one_day
print(tomorrow)  # 明日の日付を出力
```
```:実行結果
[2024-04-30, 2024-05-01, 2024-05-02, 2024-05-03, 2024-05-04]
```

# Q.11-4
**リスト内包表記を用いて、0〜99までのうち、10の倍数だけを文字列 "10s" の形式で格納せよ(例："30s")。 また、 リスト `[10, 20, 30]` に対し、各要素とその直前の要素との比(ただし初回は除く)を格納するリストを生成せよ。**

### 問題背景
#### 【1】 リスト内包表記
リスト内包表記（List Comprehension）は、短く効率的にリストを作成するためのPythonの構文。一般的な形は `[expression for item in iterable]` で、`expression` は反復可能な `iterable` の各アイテムに対して計算を行い、その結果を新しいリストとして返す。
```pythom
# 例: リスト内包表記で1から5までの数の2倍をリストにする
numbers = [x * 2 for x in range(1, 6)]
print(numbers)  # [2, 4, 6, 8, 10]
```

#### 【2】条件付きリスト内包表記
リスト内包表記において、条件式を用いて特定の要素を選択することも可能。例えば、10の倍数を選んで文字列に変換するなどの処理も、リスト内包表記で効率的に行うことができる。
```python
# 例: 0〜99の中で10の倍数だけをリストに格納
multiples_of_ten = [str(x) + "s" for x in range(0, 100) if x % 10 == 0]
print(multiples_of_ten)  # ['0s', '10s', '20s', '30s', ...]
```

#### 【3】 前の要素との比を求める
リストの各要素とその前の要素との比を計算する場合、インデックスを使って前の要素を参照することができる。`enumerate()` を使うことで、インデックスとともにリストを反復処理することもできる。
```python
# 例: 前の要素との比を計算
numbers = [10, 20, 30]
ratios = [numbers[i] / numbers[i-1] for i in range(1, len(numbers))]
print(ratios)  # [2.0, 1.5]
```
```python
# 【1】リストを定義
numbers = [10, 20, 30]

# 【2】enumerate() を使って、インデックスと値を同時に取得しながらループ
ratios = [
    numbers[i] / numbers[i-1]  # 【3】前の要素との比を計算
    for i, num in enumerate(numbers)  # 【4】enumerate() でインデックスと値を取得
    if i > 0  # 【5】iが0より大きい場合（最初の要素は無視）
]

# 【6】結果を表示
print(ratios)  # 【7】[2.0, 1.5]
```

### 解答例とコードによる実践
```python
# 【1】0〜99の範囲の数字の中から、10の倍数を文字列 "10s" の形式で格納するリストを生成
multiples_of_ten = [str(x) + "s" for x in range(0, 100) if x % 10 == 0]  # 【2】10の倍数のみを取り出し、"s" を付けて文字列にする

# 【3】結果を表示
print("10の倍数を格納したリスト:", multiples_of_ten)  # 【4】出力: ['0s', '10s', '20s', ..., '90s']

# 【5】リスト [10, 20, 30] に対し、各要素とその直前の要素との比を求める
numbers = [10, 20, 30]  # 【6】リストを定義
ratios = [numbers[i] / numbers[i-1] for i in range(1, len(numbers))]  # 【7】2番目の要素以降とその前の要素との比を計算

# 【8】結果を表示
print("直前の要素との比:", ratios)  # 【9】出力: [2.0, 1.5]
```
```: 実行結果
10の倍数を格納したリスト: ['0s', '10s', '20s', '30s', '40s', '50s', '60s', '70s', '80s', '90s']
直前の要素との比: [2.0, 1.5]
```

# Q.11-5
**辞書リスト `[{ "a": 1, "b": 2 }, { "a": 3, "b": 4 }]` に対し、キー `"a"` の値を2乗したリストを内包表記で生成せよ。また、リスト `["alice", "bob", "ALICE", "BOB"]` に対して重複を除去しつつ大文字・小文字を区別しないリストを内包表記で生成せよ。

### 問題背景
#### 【1】 辞書リストの内包表記。
辞書のリストに対して内包表記を使い、各辞書のキー `"a"` の値を2乗する処理を行う。辞書のリストにアクセスし、キー `"a"` の値を変更して新しいリストを生成する。
```python
# 例：辞書リストで、各辞書の "a" の値を2乗する
dict_list = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
squared_list = [{"a": item["a"] ** 2, "b": item["b"]} for item in dict_list]
```

#### 【2】 `set()` と重複排除
`set()` は集合を表すデータ型であり、重複を排除するために非常に有用。リストに対して `set()` を使うと、重複を自動的に除去することができる。
```python
# 例：リストから重複を取り除く
my_list = ["alice", "bob", "ALICE", "BOB"]
unique_list = list(set(my_list))  # set()で重複を除去
print(unique_list)  # ['alice', 'ALICE', 'bob', 'BOB']
```

#### 【3】 大文字/小文字を区別しない比較
大文字/小文字を区別しないようにするためには、文字列を統一的に小文字/大文字に変換して比較する。`lower()` メソッドを用いてすべて小文字にすることで、大文字/小文字の違いを無視して比較できる。
```python
# 例：大文字小文字を区別せずに重複を取り除く
my_list = ["alice", "bob", "ALICE", "BOB"]
lower_list = list(set([x.lower() for x in my_list]))
print(lower_list)  # ['alice', 'bob']
```

### 解答例とコードによる実践
```python
# 【1】辞書リストを定義
dict_list = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]  # 【2】辞書のリストを作成

# 【3】リスト内包表記で、"a" の値を2乗して新しいリストを作成
squared_list = [{"a": item["a"] ** 2, "b": item["b"]} for item in dict_list]  # 【4】辞書リストの要素を2乗する

# 【5】結果を表示
print(squared_list)  # 【6】出力: [{'a': 1, 'b': 2}, {'a': 9, 'b': 4}]
```
```: 実行結果
[{'a': 1, 'b': 2}, {'a': 9, 'b': 4}]
```

```python
# 【1】元のリストを定義
names = ["alice", "bob", "ALICE", "BOB"]  # 【2】大文字と小文字の違いで重複が発生しているリスト

# 【3】リスト内包表記で大文字小文字を無視して重複を取り除いたリストを生成
unique_names = list({name.lower() for name in names})  # 【4】lower() を使って小文字に変換し、set() で重複を除去

# 【5】結果を表示
print(unique_names)  # 【6】出力: ['alice', 'bob']
```
```: 実行結果
['alice', 'bob']
```

# あとがき
今回は「`range` 関数とループ処理」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回は、「ループ処理における `break` と `continue`」について扱う予定です。

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