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 產生新字串。 |
| 把字串當作可變容器傳入函式 | 函式內部的「修改」其實不會影響外部變數。 | 回傳新字串或使用 list、bytearray 代替。 |
| 忽略字串的雜湊特性 | 把可變物件(如 list)當作 dict 鍵會出錯。 |
確保鍵是不可變類型(str, int, tuple 等)。 |
| 大量小字串拼接 | 會導致記憶體碎片化。 | 使用 ''.join()、format()、或 f-string(在少量拼接時亦可)。 |
最佳實踐:
- 以「產生新字串」為常態:把「修改」視為「建立」的過程,讓程式邏輯更清晰。
- 使用
join()處理大量拼接:尤其在讀取檔案、產生報表或組合 SQL 語句時。 - 盡量避免在全域變數上直接重新指派:易造成不可預期的副作用,改用函式回傳新字串。
- 善用
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 框架常用字串作為快取鍵,例如 redis、memcached。因為字串不可變且可雜湊,可以安全地在多個請求之間共享。
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-string或format();若真的需要原位修改,可考慮bytearray。 - 在實務上,字串不可變性是快取鍵、日誌訊息、正則處理、跨執行緒資料傳遞等場景的基礎保證。
掌握了字串不可變性的概念與最佳實踐,你就能寫出 更安全、更高效的 Python 程式,為日後的專案開發奠定堅實的基礎。祝你在 Python 的世界裡寫出更優雅的程式碼!