本文 AI 產出,尚未審核

Python 課程 – 資料結構:字串(str)

主題:字串不可變性


簡介

在 Python 中,字串(str)是一種最常使用的資料型別。無論是處理使用者輸入、讀寫檔案,還是構造 API 回傳值,都離不開字串的操作。
然而許多初學者在使用字串時,會誤以為它可以像列表那樣直接「就地」修改,結果卻遇到 TypeError 或效能瓶頸。

理解 字串的不可變性(immutability),不只是避免錯誤,更能幫助我們寫出更安全、效能更佳的程式碼。本文將從概念說明、實作範例、常見陷阱到實務應用,一步步帶你掌握這項基礎卻關鍵的特性。


核心概念

1. 什麼是不可變性?

在 Python 中,不可變(immutable)表示物件建立後,其內容無法被直接改變。字串屬於不可變物件,這意味著:

  • 任何看似「修改」字串的操作,其實都是建立一個新字串,再把變數指向新物件。
  • 原本的字串仍然存在於記憶體中,直到沒有變數再引用它,才會被垃圾回收機制釋放。

為什麼設計成不可變?

  • 安全性:多執行緒共享同一字串時,不會因為其中一個執行緒改變內容而導致資料競爭。
  • 快取與雜湊:不可變物件的雜湊值(hash())在生命週期內不會變,讓它們可以安全作為字典(dict)或集合(set)的鍵。

2. 基本操作:建立與「修改」字串

範例 1:直接賦值與重新指向

msg = "Hello"
print(msg)          # -> Hello

# 嘗試修改第一個字元(會失敗)
# msg[0] = "h"      # TypeError: 'str' object does not support item assignment

# 正確的做法:重新建立新字串
msg = "h" + msg[1:]
print(msg)          # -> hello

這裡的 msg = "h" + msg[1:] 並未改變原本的 "Hello",而是產生了一個 全新的字串,再把變數 msg 指向它。

範例 2:使用 replace() 產生新字串

url = "https://example.com/page"
new_url = url.replace("http", "https")
print(url)      # -> https://example.com/page   (原字串未變)
print(new_url)  # -> https://example.com/page   (仍是 http,因為 replace 只會在 http 前面找)

replace() 回傳的是 新字串,原本的 url 仍保持不變。若要「修改」變數本身,必須自行把回傳值再指派回去:url = url.replace("http", "https")

範例 3:字串拼接與 join() 的效能差異

# 不建議在迴圈內使用 + 直接拼接(會產生大量中間字串)
parts = ["Python", "是", "一門", "很棒的", "語言"]
sentence = ""
for p in parts:
    sentence += p + " "      # 每次迭代都會產生新字串

print(sentence)   # -> Python 是 一門 很棒的 語言 

# 推薦使用 join():一次性建立最終字串
sentence2 = " ".join(parts)
print(sentence2)  # -> Python 是 一門 很棒的 語言

join() 只會在最後一次性建立字串,大幅降低記憶體分配與拷貝的成本,在大量資料處理時差異尤為明顯。

範例 4:字串切片產生新字串

text = "immutable"
sub = text[:4]     # 取得 "immut"
print(sub)         # -> immu
print(text)        # -> immutable(原字串未變)

切片操作會返回一個全新字串,原始 text 仍保持不變。

範例 5:利用 bytearray 變通可變字串需求

# 若真的需要「就地」修改字元,可先轉成 bytearray(可變)
data = bytearray(b"Hello")
data[0] = ord('h')
print(data)        # -> bytearray(b'hello')
# 再轉回字串
new_str = data.decode()
print(new_str)     # -> hello

bytearray 允許原位修改,但它是 二進位資料,使用前需確定字串內容皆屬於 ASCII 或已正確編碼。


3. 為什麼不可變對雜湊(hash)很重要?

key = "user:123"
cache = {key: "John"}   # 把字串作為 dict 鍵

# 嘗試修改鍵(會失敗)
# key[0] = "U"          # TypeError
# 正確的做法:重新產生新鍵
key = "user:124"
print(cache)           # 仍只有原本的 {"user:123": "John"}

若字串是可變的,字典內部的雜湊表 會因鍵值改變而失效,導致查詢錯誤或程式崩潰。因此不可變性是 字典與集合安全性的基礎


常見陷阱與最佳實踐

陷阱 說明 建議的寫法
在迴圈中使用 + 連接字串 每次迭代都會產生新字串,造成 O(n²) 的時間與記憶體開銷。 使用 ''.join(list_of_parts)io.StringIO
直接修改字串索引 會拋出 TypeError 透過切片、replace()bytearray 產生新字串。
把字串當作可變容器傳入函式 函式內部的「修改」其實不會影響外部變數。 回傳新字串或使用 listbytearray 代替。
忽略字串的雜湊特性 把可變物件(如 list)當作 dict 鍵會出錯。 確保鍵是不可變類型(str, int, tuple 等)。
大量小字串拼接 會導致記憶體碎片化。 使用 ''.join()format()、或 f-string(在少量拼接時亦可)。

最佳實踐

  1. 以「產生新字串」為常態:把「修改」視為「建立」的過程,讓程式邏輯更清晰。
  2. 使用 join() 處理大量拼接:尤其在讀取檔案、產生報表或組合 SQL 語句時。
  3. 盡量避免在全域變數上直接重新指派:易造成不可預期的副作用,改用函式回傳新字串。
  4. 善用 f-string:可讀性高且在少量拼接時效能不輸給 format()
# 推薦寫法:使用 f-string + join
items = ["apple", "banana", "cherry"]
msg = f"我喜歡的水果有: {', '.join(items)}."
print(msg)   # -> 我喜歡的水果有: apple, banana, cherry.

實際應用場景

1. 日誌(Logging)與訊息格式化

在大型系統中,日誌訊息往往需要加入時間戳、模組名稱、層級等資訊。使用不可變字串的拼接方式可以保證 多執行緒同時寫入日誌時不會互相干擾

import datetime, threading

def log(msg):
    timestamp = datetime.datetime.now().isoformat()
    thread_id = threading.get_ident()
    # 使用 f-string + join,避免在迴圈內重複 + 拼接
    entry = f"[{timestamp}] (Thread-{thread_id}) {msg}"
    print(entry)

log("系統啟動")

2. 快取鍵(Cache Key)設計

Web 框架常用字串作為快取鍵,例如 redismemcached。因為字串不可變且可雜湊,可以安全地在多個請求之間共享

def make_cache_key(user_id, page):
    return f"user:{user_id}:page:{page}"

key = make_cache_key(123, "profile")
# 直接使用於 Redis
# redis_client.set(key, profile_json)

3. 文本處理與正則表達式

在大量文字分析(如自然語言處理)時,常會先把原始文本切割成子字串,再做清理。了解不可變性可避免不必要的拷貝。

import re

raw = "Hello, World! 2023"
# 使用正則一次性清理
clean = re.sub(r"[^\w\s]", "", raw).lower()
tokens = clean.split()
print(tokens)   # -> ['hello', 'world', '2023']

4. 多執行緒資料傳遞

因為字串不可變,可以直接把字串物件放入佇列(queue.Queue,而不必擔心另一個執行緒會改變內容。

import queue, threading

q = queue.Queue()

def producer():
    for i in range(5):
        q.put(f"msg-{i}")

def consumer():
    while True:
        msg = q.get()
        if msg is None: break
        print("收到:", msg)

t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start(); t2.start()
t1.join(); q.put(None); t2.join()

總結

  • 字串在 Python 中是不可變的,任何「修改」動作其實都是產生新字串
  • 這個特性提供了執行緒安全、雜湊穩定等好處,同時也要求我們在寫程式時以建立新字串的觀念來思考。
  • 為了效能,避免在迴圈內使用 + 拼接,改用 ''.join()f-stringformat();若真的需要原位修改,可考慮 bytearray
  • 在實務上,字串不可變性是快取鍵、日誌訊息、正則處理、跨執行緒資料傳遞等場景的基礎保證。

掌握了字串不可變性的概念與最佳實踐,你就能寫出 更安全、更高效的 Python 程式,為日後的專案開發奠定堅實的基礎。祝你在 Python 的世界裡寫出更優雅的程式碼!