文字列の発展

レッスン 06 では基本を学びました。今回はさらに深く掘り下げます。イミュータビリティとは実際にはどういう意味でしょうか?あまり知られていないが強力な文字列操作にはどんなものがあるでしょうか?中国語の文字はコンピュータにどのように保存されるのでしょうか?このレッスンが終われば、文字列に対する理解が新たなレベルに達します。


1. 文字列のイミュータビリティ

レッスン 06 の FAQ で簡単に触れました——文字列は一度作成されると変更できません。これが具体的に何を意味するか見てみましょう。

PYTHON
text = "Hello"

# ❌ このように修正できない — エラーになる
# text[0] = "h"    # TypeError: 'str' object does not support item assignment

最初の文字を小文字に変更するには、新しい文字列を作成します:

PYTHON
text = "Hello"
new_text = "h" + text[1:]   # 新しい文字列を作成
print(new_text)              # hello
print(text)                  # Hello — 元の文字列は変わらない

イミュータビリティの利点

PYTHON
# 安全な共有参照
a = "Python"
b = a       # b と a は同じ文字列を指す

# a に何をしても b は影響を受けない
a = a.upper()
print(a)    # PYTHON
print(b)    # Python — b は元の値を保持
💡 ヒント:イミュータビリティはスレッドセーフハッシュ可能性をもたらします。文字列は辞書のキーにできます(後で説明します)。リストはミュータブルなので辞書のキーにはできません——変更すると辞書のルックアップが壊れるからです。

イミュータビリティの代償

「変更」するたびに新しいオブジェクトが作成されます。ループ内で多数の文字列を連結すると遅くなります:

PYTHON
# ❌ 推奨しない:ループ内での文字列連結
result = ""
for i in range(10000):
    result += str(i) + ","
# これにより 10000 個の一時的な文字列オブジェクトが作成される

# ✅ 推奨:リストに集めてから join()
parts = []
for i in range(10000):
    parts.append(str(i))
result = ",".join(parts)
⚠️ 補足:日常のコーディングで数十個の文字列を連結するのはパフォーマンス上の問題になりません。注意すべきはループ内で数千の文字列を連結する場合です。数百回以下の操作では += で全く問題ありません。

例:文字列メソッドのシミュレーション(難易度 ⭐⭐)

PYTHON
# 「capitalize」関数を実装
def my_capitalize(text):
    if not text:
        return text
    first = text[0].upper()
    rest = text[1:].lower()
    return first + rest

print(my_capitalize("hello WORLD"))      # Hello world
print(my_capitalize("python"))           # Python

# 「replace」関数を実装
def my_replace(text, old, new):
    parts = text.split(old)     # old で分割
    return new.join(parts)      # new で再結合

print(my_replace("one, two, one", "one", "1"))    # 1, two, 1
▶ 試してみよう

2. 文字列クエリメソッド

レッスン 06 の find()count() に加えて、さらに便利なメソッドを紹介します。

startswith() と endswith()

PYTHON
# 文字列の開始/終了を確認

url = "https://www.example.com"

print(url.startswith("https"))    # True — HTTPS プロトコル
print(url.endswith(".com"))       # True — .com ドメイン
print(url.endswith(".org"))       # False

# 複数のパターンを一度にチェック可能
print(url.endswith((".com", ".org", ".cn")))   # True — .com にマッチ

実用的な使い方

PYTHON
# ファイル拡張子のチェック
filename = "report.pdf"

if filename.endswith((".pdf", ".doc", ".docx")):
    print("Document file")
elif filename.endswith((".jpg", ".png", ".gif")):
    print("Image file")
elif filename.endswith((".py", ".js", ".html")):
    print("Code file")
else:
    print("Other file")

# URL プロトコルのチェック
link = "ftp://files.example.com"
if link.startswith(("http://", "https://")):
    print("Web link")
elif link.startswith("ftp://"):
    print("FTP link")

出力:

TEXT
Document file
FTP link

removeprefix() と removesuffix()(Python 3.9+)

これらは Python 3.9 で追加され、プレフィックスやサフィックスをきれいに除去します:

PYTHON
url = "https://www.example.com"

# プロトコルプレフィックスを除去
domain = url.removeprefix("https://")
print(domain)               # www.example.com

# ドメインサフィックスを除去
site_name = url.removesuffix(".com")
print(site_name)             # https://www.example

# 文字列が指定されたテキストで始まっていない/終わっていない場合は、そのまま返す
print(url.removeprefix("ftp://"))   # https://www.example.com(変更なし)
💡 ヒント:Python 3.9 より前は、プレフィックスを除去するのに url[len("https://"):]url.split("://", 1)[1] が必要でした。removeprefix()removesuffix() により、より直感的になりました。


3. 高度な split() の使い方

レッスン 06 では基本的な分割を学びました。さらに強力なバリエーションを探ってみましょう。

分割数の制限

PYTHON
# maxsplit パラメータで分割数を制限
text = "one,two,three,four,five"

print(text.split(",", 1))       # ['one', 'two,three,four,five']
print(text.split(",", 2))       # ['one', 'two', 'three,four,five']
print(text.split(",", 3))       # ['one', 'two', 'three', 'four,five']

rsplit():右から左への分割

PYTHON
text = "one,two,three,four,five"

print(text.rsplit(",", 1))      # ['one,two,three,four', 'five']
print(text.rsplit(",", 2))      # ['one,two,three', 'four', 'five']

splitlines():行ごとの分割

PYTHON
multiline = """First line
Second line
Third line"""

lines = multiline.splitlines()
print(lines)                    # ['First line', 'Second line', 'Third line']

# 改行を保持
lines = multiline.splitlines(True)
print(lines)                    # ['First line\n', 'Second line\n', 'Third line']

partition() と rpartition()

partition() は分割するだけでなく、区切り文字の位置も保持します:

PYTHON
text = "user@example.com"

# @ で分割して 3 つの部分に:(左、区切り、右)
parts = text.partition("@")
print(parts)                    # ('user', '@', 'example.com')

# 区切り文字が見つからない場合、('元の文字列', '', '') を返す
print("hello".partition("@"))   # ('hello', '', '')

# rpartition は右から検索
path = "/home/user/documents/file.txt"
last_slash = path.rpartition("/")
print(last_slash)               # ('/home/user/documents', '/', 'file.txt')

partition() は「抽出」シナリオで split() よりも優れています——3 つの部分がそれぞれ何であるかを正確に示してくれます。

例:URL の解析(難易度 ⭐⭐)

PYTHON
# partition を使って URL を解析
url = "https://www.example.com:8080/path/page.html?name=test&page=1"

# プロトコルを抽出
proto, _, rest = url.partition("://")
print(f"Protocol: {proto}")        # https

# ホストとパスを抽出
host_part, _, path_and_query = rest.partition("/")
print(f"Host: {host_part}")    # www.example.com:8080

# ポートを抽出
host, _, port = host_part.partition(":")
print(f"Domain: {host}")         # www.example.com
print(f"Port: {port}")         # 8080

# パスとクエリを抽出
path, _, query = path_and_query.partition("?")
print(f"Path: /{path}")        # /path/page.html
print(f"Query: {query}")        # name=test&page=1
▶ 試してみよう

出力:

TEXT
Protocol: https
Host: www.example.com:8080
Domain: www.example.com
Port: 8080
Path: /path/page.html
Query: name=test&page=1

4. 文字列の配置とパディング

Python は指定した幅の中で文字列を配置するメソッドを提供します——左詰め、右詰め、中央揃え。

PYTHON
text = "Python"

print(text.ljust(10))           # 'Python    '(左詰め、右側にスペース)
print(text.rjust(10))           # '    Python'(右詰め、左側にスペース)
print(text.center(10))          # '  Python  '(中央揃え、両側にスペース)

# 埋め文字を指定可能
print(text.ljust(10, "-"))      # 'Python----'
print(text.rjust(10, "-"))      # '----Python'
print(text.center(10, "-"))     # '--Python--'

zfill():ゼロ埋め

数値 ID によく使われます:

PYTHON
print("42".zfill(5))            # 00042
print("-42".zfill(5))           # -0042 — 負号が先頭
print("3.14".zfill(8))          # 00003.14

# 3 桁の ID を生成
for i in range(1, 11):
    filename = f"photo_{str(i).zfill(3)}.jpg"
    print(filename, end="  ")

出力:

TEXT
photo_001.jpg  photo_002.jpg  photo_003.jpg  photo_004.jpg  photo_005.jpg
photo_006.jpg  photo_007.jpg  photo_008.jpg  photo_009.jpg  photo_010.jpg

例:配置したレポートの生成(難易度 ⭐⭐)

PYTHON
# rjust と ljust を使って表を配置
items = [
    ("Apple", 5.0, 3),
    ("Banana", 3.5, 5),
    ("Milk", 12.0, 2),
]

# ヘッダー
header_name = "Item".ljust(8)
header_price = "Price".rjust(6)
header_qty = "Qty".rjust(4)
header_total = "Total".rjust(6)
print(f"{header_name}{header_price}{header_qty}{header_total}")
print("-" * 28)

# データ行
for name, price, qty in items:
    total = price * qty
    name_col = name.ljust(8)
    price_col = f"{price:.1f}".rjust(6)
    qty_col = str(qty).rjust(4)
    total_col = f"{total:.1f}".rjust(6)
    print(f"{name_col}{price_col}{qty_col}{total_col}")
▶ 試してみよう

出力:

TEXT
Item     Price  Qty Total
────────────────────────────
Apple      5.0    3  15.0
Banana     3.5    5  17.5
Milk      12.0    2  24.0

5. 文字エンコーディング:ord() と chr()

コンピュータ内のすべての文字は数値(コードポイント)に対応しています。ord() は文字の数値エンコーディングを取得し、chr() はその逆を行います。

PYTHON
# Unicode コードポイントを表示
print(ord("A"))          # 65
print(ord("中"))         # 20013
print(ord("❤"))          # 10084

# コードポイントから文字を取得
print(chr(65))           # A
print(chr(20013))        # 中
print(chr(10084))        # ❤

# すべての大文字英字を生成
for code in range(ord("A"), ord("Z") + 1):
    print(chr(code), end=" ")
# 出力:A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Unicode とは何か

簡単に言うと、Unicode は世界共通の文字エンコーディング標準です——世界中の書記体系(中国語、英語、アラビア語、日本語など)のすべての文字に一意の番号を割り当てています。Python 3 はすべての文字列を内部で Unicode として保存するため、複数の言語をネイティブにサポートしています。

PYTHON
# Python 3 の文字列はネイティブに Unicode をサポート
text = "Hello, 你好, مرحبا, こんにちは"

print(text)                     # すべて正しく表示される
print(len(text))                # 文字数

# \u エスケープで文字を表現することもできる
print("\u0048")                 # H
print("\u4e2d\u6587")          # 中文(中国語)
⚠️ 補足:Python 2 では、文字列は str(バイト)と unicode(テキスト)に分かれており、エンコーディングに関するエラーが頻発していました。Python 3 ではこれらが統合されました——すべての文字列は Unicode であり、中国語と英語を同等に扱います。これは Python 3 の最も優れた設計上の決定の 1 つです。

エンコードとデコード

Python は内部で Unicode を使用しますが、ファイル保存やネットワーク送信ではバイトが必要です。そのためエンコード(encode)とデコード(decode)が必要です:

PYTHON
text = "Hello, 世界"

# エンコーディング:文字列 → バイト
utf8_bytes = text.encode("utf-8")
print(utf8_bytes)               # b'\xe4\xbd\xa0\xe5\xa5\xbd...'
print(len(utf8_bytes))          # UTF-8 でのバイト数

gbk_bytes = text.encode("gbk")
print(len(gbk_bytes))           # GBK でのバイト数

# デコーディング:バイト → 文字列
decoded = utf8_bytes.decode("utf-8")
print(decoded)                  # Hello, 世界

# エンコードとデコードが一致しないとエラーになる
# gbk_bytes.decode("utf-8")    # UnicodeDecodeError!
💡 ヒント:日常の開発の 99% は UTF-8 で十分です。UTF-8 はインターネット標準のエンコーディングで、ASCII と互換性があり、すべての Unicode 文字を表現できます。ファイル読み込み時に文字化けが発生した場合、ほぼ確実にエンコーディングの問題です——ファイルを開くときに encoding="utf-8" を指定すれば解決します。


6. 文字列書式設定の補足

f-string 以外に、Python には 2 つの古い書式設定方法があります。f-string が現代の標準ですが、古いコードでこれらに遭遇することがあります。

format() メソッド(Python 3.0+)

PYTHON
# 位置による指定
print("My name is {}, I'm {} years old.".format("Alice", 18))
# My name is Alice, I'm 18 years old.

# 名前による指定
print("My name is {name}, I'm {age} years old.".format(name="Bob", age=22))
# My name is Bob, I'm 22 years old.

# 数値の書式設定
print("Pi: {:.3f}".format(3.14159))
# Pi: 3.142

% 書式設定(Python 2 時代)

PYTHON
# 古いコードで見かけることがある
print("My name is %s, I'm %d years old." % ("Alice", 25))
# My name is Alice, I'm 25 years old.

print("Pi: %.2f" % 3.14159)
# Pi: 3.14
💡 ヒント:f-string(Python 3.6+)が最良の選択です——最速、最も読みやすく、最も簡潔です。新しいコードでは常に f-string を使用してくださいformat() は「遅延書式設定」(テンプレートを先に定義し、後で値を埋める)にまだ便利です。% 書式設定は遭遇したときに認識できるようにするためだけのもので、新しいコードでは使わないでください。


よくあるユースケース


❓ よくある質問

Q なぜ Python 3 の文字列は Unicode で、Python 2 の文字列は Unicode ではなかったのですか?
A Python 2 の時代は、エンコーディング(ASCII、Latin-1、GBK、Shift-JIS が混在)の混乱に悩まされていました。文字列の処理は「バイト」と「テキスト」に分かれており——2 つの別々のシステムで、非常にエラーが発生しやすかったのです。Python 3 はきれいに断ち切りました:すべての文字列は Unicode(テキスト)です。バイトデータは bytes 型で別途処理されます。これにより文字列操作が直感的になります——len("中") はどのシステムでも常に 1 です。トレードオフとして、ファイルやネットワーク I/O では手動での encode/decode が必要です。
⚠️ Q:split()partition() はいつ使い分ければよいですか? A:単純な分割には split() を使います。区切り文字そのものが必要な場合は partition() を使います。partition() は常に 3 つの要素(左、区切り、右)を返すため、タプルアンパックに便利です:left, sep, right = text.partition(":")split() は可変長のリストを返します。区切り文字が 1 回だけ出現し、両側の内容が必要な場合は、partition() が最適です。

Q ljust() と f-string の {value:10} の違いは何ですか?
A 同じ結果を生成します——text.ljust(10)f"{text:<10}" と同じです。f-string 版はより柔軟(配置と数値書式設定を組み合わせられる)ですが、メソッド呼び出しの方が直感的です。好みの問題です——1 つ選んで一貫して使いましょう。f-string の配置記号:< 左詰め、> 右詰め、^ 中央揃え:f"{text:^10}"
Q UTF-8 と GBK の違いは何ですか?Python ファイルを保存するときに文字化けするのはなぜですか?
A UTF-8 は国際標準です——1~4 バイト/文字で、すべての文字に対応します。GBK は中国語の標準(簡体字)です——1~2 バイト/文字で、中国語と英語のみをサポートします。Python ファイルが GBK で保存されていて、日本語やアラビア語のテキストが含まれていると、エラーになったり文字化けしたりします。解決策:すべての Python ファイルを UTF-8 で保存してください——Python 3 のデフォルトは UTF-8 です。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐)files = ["report.pdf", "photo.jpg", "script.py", "notes.txt", "index.html"] として、endswith() を使ってフィルタリングしてください:

    • すべての画像ファイル(.jpg, .png, .gif)
    • すべてのドキュメントファイル(.pdf, .doc, .docx, .txt)
    • すべてのコードファイル(.py, .js, .html, .css)
  2. 中級(難易度 ⭐⭐):「バッチファイル名変更プログラム」を書いてください。photos = ["IMG_1.jpg", "IMG_2.jpg", ..., "IMG_12.jpg"]photo_001.jpgphoto_002.jpg、...、photo_012.jpg に変更します。ヒントpartition() でファイル名と拡張子を分割、zfill() でゼロ埋め。

  3. 挑戦(難易度 ⭐⭐⭐):「簡易テンプレートエンジン」を書いてください。テンプレート文字列 template と辞書 data が与えられ、{variable} プレースホルダを実際の値に置き換えます。

    PYTHON
    template = "Dear {name}, your order {order_id} has been {status}. Estimated delivery: {delivery_time}."
    
    data = {
        "name": "Alice",
        "order_id": "20260623001",
        "status": "shipped",
        "delivery_time": "3 business days"
    }
    
    # あなたのコード:replace_template(template, data) を実装
    # 出力:Dear Alice, your order 20260623001 has been shipped. Estimated delivery: 3 business days.
    

    追加要件:テンプレート変数が data に存在しない場合、{variable} はそのままにします(置換しない)。ヒント:data のキーと値のペアを反復処理し、テンプレートに対してそれぞれ replace() を呼び出します。

100%