演習形式で学ぶPythonプログラミング vol.5 ~format()とf-string~

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

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

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

今回は「format()とf-string」について扱います。それでは始めましょう！

# Q.5-1
##### `str.format()` を用いて、辞書型のデータから動的にキーを指定して `format()` を構築せよ。ただし、キーが存在しない場合は `"N/A"` と表示されるようにせよ。

### 問題背景：`str.format()`と辞書の動的アクセス
#### [1] `str.format()` の基本構文
`"{}".format(value)` のように、文字列中の `{}` を値で置き換えることが出来る。`{key}` のようにkey名を指定することも可能。
#### [2] 辞書と`format()` の連携
`format_map()`や辞書を用いることで、辞書の `key` を `{}` に動的に挿入できる。
#### [3] `key` が存在しない場合の対処
通常の辞書では、存在しない `key` を参照すると `KeyError`が発生する。このような場合は、`collections.defaultdict` か `dict` の `get()` を併用するか、カスタム辞書クラスを作ると回避可能。

### 解答例とコードによる実践
```python
# 【1】必要なモジュールをインポート
from collections import defaultdict  # 【2】defaultdict を使ってデフォルト値を設定するため

# 【3】辞書データを作成（例: 一部のキーが存在しない）
data = {"name": "Alice", "age": 30}  # "job" は存在しない

# 【4】defaultdict を使用して、存在しないキーに "N/A" を設定する
safe_data = defaultdict(lambda: "N/A", data)

# 【5】フォーマット文字列を作成
template = "Name: {name}, Age: {age}, Job: {job}"

# 【6】format_map を使い、辞書の値を動的に挿入
result = template.format_map(safe_data)

# 【7】結果を表示
print(result)  # → Name: Alice, Age: 30, Job: N/A
```
```:出力結果
Name: Alice, Age: 30, Job: N/A
```

### 補足
#### [1] `dict.get()` を用いた別解
```python
data = {"name": "Alice", "age": 30}
template = "Name: {name}, Age: {age}, Job: {job}"

result = template.format(
    name=data.get("name", "N/A"),
    age=data.get("age", "N/A"),
    job=data.get("job", "N/A")
)
print(result)  # → Name: Alice, Age: 30, Job: N/A
```
ただしこちらは `key` ごとに明示的に `get()` を記述する必要があり、スケーラビリティが劣る。

#### [2] `f-string` を用いた書き換えはできない
上記のコードは、`f-string` を用いて書き換えすることはできない。その理由は以下の2つ。
##### 1)「Pythonの変数や式を `{}`内で評価する」仕組み

```python
name = "Alice"
age = 30
job = "N/A"
result = f"Name: {name}, Age: {age}, Job: {job}"
```
上記は、変数 `name`, `age`, `job` が存在することが前提である。

##### 2) 今回の `format_map()` の動作
`format_map()` は辞書を用いて動的に値を参照する方法。もし `key` が存在しない場合でも、`defaultdict` を利用することで、`"N/A"` などを返すことが出来る。
```python
safe_data = defaultdict(lambda: "N/A", data)
result = template.format_map(safe_data)
```
ここでは、`key` が存在しない場合に、`"N/A"`を返す処理が仕込まれている。`f-string` には、このような「辞書で動的に参照して、存在しない場合にデフォルトを返す」ような機能はない。

# Q.5-2
#####  `f-string` 内で辞書の値を動的に参照する構文を示し、キーが変数である場合に注意すべき点を含めて実装せよ。

### 問題背景： `f-string` における辞書の値の参照と変数キー
#### [1] `f-string` の基本
`f"..."` 内の `{}` で変数や式を直接評価する構文。Python 3.6以降で使用可能
#### [2] 辞書の値を参照する場合
`data["key"]` の形で参照できる。ただし `key` が変数(e.g.) `key = "name"`)の場合は、`data[key]` のように変数名でアクセスする必要がある。
#### [3] 注意点
`f-string` の `{}` 内は式であるため、必ず `data[key]` で明示的に書く必要がある。`data["key"]` と `date[key]` と書かないと変数名を用いた参照にならない。`date["key"]` と書くと「 `key` というキー名」として扱われる。

### 解答例とコードを用いた実践
```python
# 【1】辞書データを作成する
data = {"name": "Alice", "age": 30, "job": "Engineer"}

# 【2】参照したいキーを変数として指定する
key = "job"

# 【3】f-string 内で辞書の値を動的に参照する（キーが変数である場合）
result = f"職業: {data[key]}"  # 【4】key という変数を使って値を取得

# 【5】結果を表示
print(result)  # → 職業: Engineer

# 【6】キーが存在しない可能性がある場合
result_safe = f"職業: {data.get(key, '未設定')}"  # 【7】get() を使って存在しない場合は '未設定' とする

print(result_safe)  # → 職業: Engineer

```
```:出力結果
職業: Engineer
```

# Q.5-3
##### `str.format()` と `f-string` におけるフィールド名の再利用とインデックスの混在使用に関する挙動を、複数のプレースホルダを用いたコードでそれぞれ確認せよ。

### 問題背景：`str.format()` と `f-string` のプレースホルダの使い方
`str.format()` は、インデックス指定と名前指定の両方を用いることが出来るは、自動インデックス `{}` と混在させるとエラーになる。一方、`f-string` は名前や式だけで構成されるため、インデックスの概念は存在しないが、listやtupleのインデックスを直接指揮で指定可能。
上記を含めて、違いは以下。

| 比較項目              | `str.format()`                               | `f-string`                                         |
|----------------------|-----------------------------------------------|---------------------------------------------------|
| インデックス指定     | `{0}`, `{1}` で指定                           | `リスト[0]`, `タプル[1]` のように式で指定         |
| 名前指定             | `{name}`                                      | 変数名をそのまま `{name}` に書く                  |
| 再利用               | 何度でも `{0}`, `{name}` を再利用可能         | 何度でも同じ式や変数を書いて再利用可能           |
| 混在                 | インデックスと名前は混在可能（空 `{}` は不可）| 式として記述するため混在の概念は存在しない       |
| エラーになるケース   | 空 `{}` とインデックス・名前の混在             | 特にない                                           |

### 解答例とコードによる実践
```python
# 【1】位置引数とキーワード引数を用意
fruit = "apple"
quantity = 5
unit_price = 120

# 【2】インデックスとキーワードを混在しつつ、同じフィールド名を再利用する例
message = "商品: {0}, 数量: {quantity}, 単価: {unit_price}円, 商品名（再利用）: {0}, 合計: {1}".format(
    fruit, quantity * unit_price, quantity=quantity, unit_price=unit_price
)

# 【3】結果を表示
print(message)  # → 商品: apple, 数量: 5, 単価: 120円, 商品名（再利用）: apple, 合計: 600

# 【4】空 {} とインデックスの混在を試みる（これはエラーになる）
# error_message = "商品: {}, 数量: {0}".format(fruit, quantity)  # ValueError
# 【5】コメントアウトしているが、実行すると ValueError となる

# 【6】タプルで値をまとめて定義
product_info = ("apple", 5, 120)  # 【7】0: fruit, 1: quantity, 2: unit_price

# 【8】f-string でインデックス（式）と再利用を行う
fstring_message = (
    f"商品: {product_info[0]}, 数量: {product_info[1]}, 単価: {product_info[2]}円, "
    f"商品名（再利用）: {product_info[0]}, 合計: {product_info[1] * product_info[2]}"
)

# 【9】結果を表示
print(fstring_message)  # → 商品: apple, 数量: 5, 単価: 120円, 商品名（再利用）: apple, 合計: 600

# NG: 空 {} とインデックスの混在（str.format のみの問題）
# print("商品: {}, 数量: {0}".format(fruit, quantity))  # ValueError
# → 自動割り当て {} を使う場合は、すべてのプレースホルダが {} でなければならない。
```
```:実行結果
商品: apple, 数量: 5, 単価: 120円, 商品名（再利用）: apple, 合計: 600
商品: apple, 数量: 5, 単価: 120円, 商品名（再利用）: apple, 合計: 600
```

# Q.5-4
##### `str.format()`, `f-string` で `datetime` モジュールを用いた日付の整形をそれぞれ行い、曜日・時刻・ISO形式の3種類のフォーマットを1行で表示せよ。

### 問題背景：日付生成と `datetime` モジュール
#### [1] `datetime` モジュール
`datetime.now()` で現在日時が取得可能。`.strftime()` メソッドを用いることで、日時を様々な形に生成できる。

| フォーマット指定 | 意味                 | 例                      |
|------------------|----------------------|------------------------|
| `%A`             | 曜日（英語）          | Monday                 |
| `%H:%M:%S`       | 時刻（24時間表記）    | 13:45:30               |
| `.isoformat()`   | ISO 8601 形式         | 2024-04-22T13:45:30    |

まとめると、`strftime()` は日付・時刻を好きなフォーマットで出力するための関数。`f-string` は `{}` の中で式が評価されるため、`.strftime()` をそのまま用いることが可能。`str.format()` でも `{}` の中で、`.strftime()` を用いることが可能。

### 解答例とコードによる実践
```python
# 【1】datetime モジュールをインポート
from datetime import datetime

# 【2】現在時刻を取得
now = datetime.now()

# 【3】str.format() を使って、曜日・時刻・ISO形式を1行で整形する
formatted_str = "Today is {0:%A}, Time: {0:%H:%M:%S}, ISO: {0:%Y-%m-%dT%H:%M:%S}".format(now)

# 【4】結果を表示
print(formatted_str)

# 【5】f-string を使って、曜日・時刻・ISO形式を1行で整形する
formatted_fstring = (
    f"Today is {now.strftime('%A')}, Time: {now.strftime('%H:%M:%S')}, ISO: {now.isoformat(timespec='seconds')}"
)

# 【6】結果を表示
print(formatted_fstring)
```
```:出力結果
Today is Monday, Time: 13:45:30, ISO: 2024-04-22T13:45:30
Today is Monday, Time: 13:45:30, ISO: 2024-04-22T13:45:30
```

`str.format()` では `{0:%A}` のように、`{index:書式}` の形で `strftime` 指定子が使える。

`f-string` では、`.strftime()` を `{}` の中で関数呼び出しする形で使用。

ISO形式については、`datetime.isoformat()` を用いる( `timespec='seconds'` で秒までの指定)。

# Q.5-5
##### `str.format()` , `f-string` のそれぞれにおいて、フィールド幅・整列・ゼロ埋めなどの書式指定子の組み合わせをすべて試し、出力を検証せよ。

### 問題背景：書式指定子（format specification mini-language）
書式指定子は、文字列整形において以下のような構成を持つ：

```
{value:[fill][align][sign][#][0][width][,][.precision][type]}
```
以下は良く用いる書式指定子である。

| 指定子   | 意味                           | 例（数値 42）             |
|----------|--------------------------------|-------------------------|
| `>`      | 右詰め（デフォルト）           | `'       42'`           |
| `<`      | 左詰め                         | `'42       '`           |
| `^`      | 中央揃え                       | `'   42    '`           |
| `0`      | ゼロ埋め（右詰めとセット）     | `'00042'`               |
| `+`      | 正負符号付き表示               | `'+42'`, `'-42'`        |
| `-`      | 負数のみ符号をつける           | `'42'`, `'-42'`         |
| `,`      | 3桁区切り                      | `'1,234,567'`           |
| `.2f`    | 小数点以下2桁（固定小数点）    | `'3.14'`                |
| `10`     | フィールド幅10                 | `'        42'`          |

`str.format()` では `{0:>5}` のように `:` の後に書式指定子を追加する。`f-string` では、`f"{value:>5}"` のように `{}` 内に直接書式指定子を記述する。
両者は同じ書式仕様に従うため、記述方法の違いだけ理解すれば使い分け可能。

### 解答例とコードによる実践
```python
# 【1】対象となる整数と浮動小数点数を定義
n = 42
f = 3.14159

# 【2】str.format() による各種フォーマット
print("【str.format() による整形】")
print("右詰め（幅10）       :", "{0:>10}".format(n))         # 【3】
print("左詰め（幅10）       :", "{0:<10}".format(n))         # 【4】
print("中央揃え（幅10）     :", "{0:^10}".format(n))         # 【5】
print("ゼロ埋め（幅6）      :", "{0:06}".format(n))           # 【6】
print("正負符号付き         :", "{0:+}".format(n))            # 【7】
print("負数の符号のみ       :", "{0:-}".format(n))            # 【8】
print("カンマ区切り         :", "{0:,}".format(1234567))      # 【9】
print("小数点以下2桁        :", "{0:.2f}".format(f))          # 【10】
print("複合指定（幅10ゼロ埋め）:", "{0:010.2f}".format(f))    # 【11】

# 【12】f-string による各種フォーマット
print("\n【f-string による整形】")
print(f"右詰め（幅10）       : {n:>10}")                      # 【13】
print(f"左詰め（幅10）       : {n:<10}")                      # 【14】
print(f"中央揃え（幅10）     : {n:^10}")                      # 【15】
print(f"ゼロ埋め（幅6）      : {n:06}")                        # 【16】
print(f"正負符号付き         : {n:+}")                         # 【17】
print(f"負数の符号のみ       : {n:-}")                         # 【18】
print(f"カンマ区切り         : {1234567:,}")                   # 【19】
print(f"小数点以下2桁        : {f:.2f}")                       # 【20】
print(f"複合指定（幅10ゼロ埋め）: {f:010.2f}")                # 【21】
```
```:出力結果
【str.format() による整形】
右詰め（幅10）       :         42
左詰め（幅10）       : 42        
中央揃え（幅10）     :    42    
ゼロ埋め（幅6）      : 000042
正負符号付き         : +42
負数の符号のみ       : 42
カンマ区切り         : 1,234,567
小数点以下2桁        : 3.14
複合指定（幅10ゼロ埋め）: 0000003.14

【f-string による整形】
右詰め（幅10）       :         42
左詰め（幅10）       : 42        
中央揃え（幅10）     :    42    
ゼロ埋め（幅6）      : 000042
正負符号付き         : +42
負数の符号のみ       : 42
カンマ区切り         : 1,234,567
小数点以下2桁        : 3.14
複合指定（幅10ゼロ埋め）: 0000003.14
```

上記をまとめると以下。
| 項目           | `str.format()`                                | `f-string`                           |
|----------------|------------------------------------------------|---------------------------------------|
| 書式構文       | `"{}".format(value)` + `:指定子`               | `f"{value:指定子}"`                  |
| 可読性         | やや冗長だが互換性高い                         | 簡潔で直感的                         |
| 書式指定子     | 共通（`<`, `>`, `^`, `0`, `.2f`, `,` など）    | 共通（`<`, `>`, `^`, `0`, `.2f`, `,` など） |


# Q.5-6
##### `str.format()` 、`f-string` で関数呼び出しやリスト内包表記を {} の中で使うときの制約と挙動を確認するコードをそれぞれ記述せよ。

### 問題背景
#### [1] `str.format()` と `f-string` の違いと共通点
いずれも文字列の中に変数や計算結果を埋め込むための手法。`{}` の中に式を書くことが出来る。ここで用いることが出来るのは、関数呼び出しとリスト内包表記(`f-string` のみ)。
まとめると、 `f-string` は `{}` の中で「何でも計算できる」。一方で、`str.format()` の `{}` は「インデックス」か「変数名」で、複雑な式は先に計算しておかないと使えない。

|                    | 関数呼び出し               | リスト内包表記                  |
|--------------------|---------------------------|---------------------------------|
| `str.format()`     | できる（変数経由で）       | できない（事前に変数へ）        |
| `f-string`         | できる（そのまま書ける）   | できる（そのまま書ける）        |

### 解答例とコードによる実践
```python
# 【1】リストを用意する
data = [1, 2, 3, 4, 5]  # 1から5までの整数をリストとして準備

# 【2】リスト内包表記の結果を別変数に保存する（str.formatでは直接中で計算できないため）
squared = [x**2 for x in data]  # 各要素を2乗してリストにする

# 【3】str.format() の中では squared のような変数名のみが使用可能
formatted_str = "Squared values: {}".format(squared)  # 変数名を渡して文字列にする

# 【4】結果を出力する
print(formatted_str)  # 2乗した結果のリストを表示

# 【5】関数を定義する（引数を2乗する関数）
def square(x):  # 1つの引数を取り、その2乗を返す
    return x ** 2

# 【6】f-stringの中で関数呼び出しを使う
formatted_fstring_func = f"The square of 7 is {square(7)}"  # 関数squareを呼び出してその結果を埋め込む

# 【7】結果を表示する
print(formatted_fstring_func)  # square(7) の結果（49）が表示される

# 【8】f-stringの中でリスト内包表記を使う（直接計算できる）
formatted_fstring_list = f"Squared values: {[x**2 for x in data]}"  # リスト内包表記をそのまま {} の中で使う

# 【9】結果を表示する
print(formatted_fstring_list)  # 2乗したリストを表示する
```
```:出力結果
Squared values: [1, 4, 9, 16, 25]
The square of 7 is 49
Squared values: [1, 4, 9, 16, 25]
```

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

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