你可能聽過「雜湊」這個詞,並且覺得它跟「加密」是同一回事——這是一個非常常見的誤解!
在 Python 中,「雜湊」這個詞其實指的是兩種完全不同的東西:
| 類型 | 工具 | 用途 | 你可能聽過的場景 |
|---|---|---|---|
| 資料結構雜湊 | hash() 函數 | 讓字典、集合能快速查找 | 「字典的鍵必須可雜湊」 |
| 密碼學雜湊 | hashlib 模組 | 密碼儲存、檔案驗證 | 「密碼要用雜湊加密」 |
這兩種雜湊的目的完全不同! 很多教學文章只講其中一種,或者混在一起講,導致初學者容易混淆。
本文會分開介紹這兩種雜湊,讓你清楚知道什麼場景用什麼。
資料結構雜湊(hash() 函數)
這部分跟「加密」完全無關!純粹是為了讓程式跑更快。
核心問題:為什麼字典查找這麼快?
想像你有一排 100 個置物櫃,你要把東西存進去,之後還要能快速找到。
方法一:隨便放,找的時候從頭找
存:把「王小明的資料」隨便塞進某一格
找:從第 1 格開始打開看... 不是... 第 2 格... 不是...
(最慘要開 100 格才找到)方法二:用規則決定放哪格
存:「王小明」→ 經過某個計算 → 得到數字 23 → 放進第 23 格
找:「王小明」→ 同樣的計算 → 得到數字 23 → 直接開第 23 格 → 找到了!這個「某個計算」就是雜湊函數,算出來的數字就是雜湊值。
因為同樣的輸入永遠得到同樣的結果,所以:
- 存的時候算一次,知道放哪格
- 找的時候再算一次,直接去那格拿
Python 的字典(dict)就是用方法二! 這就是為什麼 dict 查找超快——不管有多少資料,都只需要「算一次 + 開一格」。
hash() 函數的唯一目的:產生一個數字索引
# hash() 把任何東西變成一個整數
print(hash("王小明")) # → 某個整數,例如 12345
print(hash(42)) # → 42
print(hash((1, 2, 3))) # → 某個整數這個整數會被用來決定資料存在字典/集合的「哪一格」。
hash() 的特性(只有兩個重要的)
1. 確定性:相同輸入 → 相同輸出
# 同一次程式執行中,相同的值一定得到相同的雜湊
hash("Hello") == hash("Hello") # 永遠是 True為什麼重要?因為你放進字典的鍵,之後要能找回來!
2. 快速計算
hash() 的計算非常快,不會拖慢程式。
就這樣!沒有什麼「不可逆」「雪崩效應」
對於 hash() 函數來說:
- ❌ 不需要「不可逆」——沒人想從雜湊值反推原始資料
- ❌ 不需要「雪崩效應」——只要能均勻分散就好
- ❌ 不需要「安全性」——這不是拿來保護資料的
hash() 的存在意義只有一個:讓字典和集合跑得快。
可雜湊 vs 不可雜湊(重要觀念!)
這是一個關鍵概念:只有「不可變」的物件才能被雜湊!
✅ 可雜湊(Hashable)的類型
# 這些都可以雜湊
hash(123) # 整數 ✅
hash(3.14) # 浮點數 ✅
hash("Hello") # 字串 ✅
hash((1, 2, 3)) # 元組(內容也要是可雜湊的)✅
hash(frozenset([1, 2])) # 凍結集合 ✅
hash(True) # 布林值 ✅
hash(None) # None ✅❌ 不可雜湊(Unhashable)的類型
# 這些會報錯!
hash([1, 2, 3]) # 列表 ❌ → TypeError: unhashable type: 'list'
hash({1, 2, 3}) # 集合 ❌ → TypeError: unhashable type: 'set'
hash({"a": 1}) # 字典 ❌ → TypeError: unhashable type: 'dict'為什麼可變物件不能雜湊?
延續前面的置物櫃比喻:
存:「王小明」→ 計算得到 23 → 資料放進第 23 格現在假設「王小明」這個名字可以被修改(就像列表可以 append):
修改:「王小明」被改成「王小明明」
找:「王小明明」→ 計算得到 67 → 去開第 67 格 → 空的!?資料明明還在第 23 格,但因為名字變了,算出來的位置也變了,就再也找不到了!
這就是為什麼 Python 規定:只有不能被修改的東西,才能當字典的鍵。
# 假設列表可以當鍵(實際上 Python 不允許)
my_list = [1, 2, 3]
my_dict = {my_list: "value"} # 假設這行可以執行
# [1, 2, 3] 計算出雜湊值 → 假設是 99 → 資料存在第 99 格
my_list.append(4) # 列表變成 [1, 2, 3, 4]
# 現在要找 my_list 對應的值
# [1, 2, 3, 4] 計算出雜湊值 → 變成 42 → 去第 42 格找 → 找不到!
# 但資料其實還在第 99 格...實際應用:字典的鍵必須可雜湊
# ✅ 正確:用不能修改的東西當鍵
student_scores = {
"Alice": 95, # 字串不能修改 ✅
"Bob": 87, # 字串不能修改 ✅
(2024, "Spring"): "Semester 1" # 元組不能修改 ✅
}
# ❌ 錯誤:列表可以被修改,所以不能當鍵
wrong_dict = {
[1, 2, 3]: "value"
}
# TypeError: unhashable type: 'list'實際應用:集合的成員必須可雜湊
# ✅ 正確用法
my_set = {1, 2, "hello", (3, 4)}
# ❌ 錯誤用法
wrong_set = {[1, 2, 3]} # 列表不能放進集合!
# TypeError: unhashable type: 'list'密碼學雜湊(hashlib 模組)
這才是你可能想到的「加密相關」的雜湊!
hash() vs hashlib 的差異
| 特性 | hash() | hashlib |
|---|---|---|
| 用途 | 資料結構(dict, set) | 安全應用(密碼、驗證) |
| 輸出 | Python 整數 | 十六進位字串 |
| 跨平台一致性 | ❌ 每次執行可能不同 | ✅ 永遠一致 |
| 安全性 | 低(不適合安全用途) | 高(密碼學等級) |
| 演算法 | Python 內建 | MD5, SHA-256 等標準演算法 |
密碼學雜湊的特性(這些才是「加密」會用到的)
以下這些特性是密碼學雜湊才需要的,hash() 函數不需要:
1. 單向性(不可逆)
從雜湊值無法反推原始資料。
import hashlib
# 知道 SHA-256("password") = "5e884898da..."
# 但無法從 "5e884898da..." 反推出 "password"2. 雪崩效應
輸入只要改一點點,輸出就完全不同。
import hashlib
print(hashlib.sha256(b"Hello").hexdigest()[:16]) # → 185f8db32271fe25
print(hashlib.sha256(b"Hello!").hexdigest()[:16]) # → 334d016f755cd6dc
# 只差一個驚嘆號,結果天差地遠!3. 抗碰撞性
很難找到兩個不同的輸入產生相同的輸出。
4. 固定長度輸出
無論輸入多大,輸出長度都一樣。
import hashlib
# 不管輸入多長,SHA-256 輸出永遠是 64 個十六進位字元
hashlib.sha256(b"A").hexdigest() # 64 字元
hashlib.sha256(b"A" * 1000000).hexdigest() # 還是 64 字元常見的雜湊演算法
import hashlib
message = "Hello, World!".encode('utf-8') # 必須是 bytes
# MD5(較舊,不建議用於安全用途)
print("MD5:", hashlib.md5(message).hexdigest())
# 輸出: 65a8e27d8879283831b664bd8b7f0ad4
# SHA-1(已不安全,但仍常見)
print("SHA-1:", hashlib.sha1(message).hexdigest())
# 輸出: 0a0a9f2a6772942557ab5355d76af442f8f65e01
# SHA-256(目前主流,推薦使用)
print("SHA-256:", hashlib.sha256(message).hexdigest())
# 輸出: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
# SHA-512(更長,安全性更高)
print("SHA-512:", hashlib.sha512(message).hexdigest())
# 輸出: 374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387應用一:安全儲存密碼
永遠不要用明碼儲存密碼!使用雜湊來保護使用者密碼。
import hashlib
def hash_password(password):
"""將密碼轉換成雜湊值"""
return hashlib.sha256(password.encode('utf-8')).hexdigest()
def verify_password(password, stored_hash):
"""驗證密碼是否正確"""
return hash_password(password) == stored_hash
# 使用範例
# 註冊時
user_password = "MySecretPassword123"
stored_hash = hash_password(user_password)
print(f"儲存在資料庫的雜湊: {stored_hash}")
# 登入時
login_attempt = "MySecretPassword123"
if verify_password(login_attempt, stored_hash):
print("登入成功!")
else:
print("密碼錯誤!")⚠️ 進階提醒:實際生產環境應該使用「加鹽」(Salt) 和專門的密碼雜湊函數如
bcrypt或argon2,以防止彩虹表攻擊。
應用二:檔案完整性驗證
確認下載的檔案沒有被篡改。
import hashlib
def calculate_file_hash(filepath):
"""計算檔案的 SHA-256 雜湊值"""
sha256_hash = hashlib.sha256()
with open(filepath, "rb") as f:
# 分塊讀取,避免大檔案佔用太多記憶體
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
# 使用範例
file_hash = calculate_file_hash("my_file.zip")
print(f"檔案雜湊值: {file_hash}")
# 比對官方提供的雜湊值
official_hash = "abc123..." # 從官網取得
if file_hash == official_hash:
print("✅ 檔案完整,未被篡改")
else:
print("❌ 警告:檔案可能已被修改!")應用三:資料去重
利用雜湊值快速判斷資料是否重複。
import hashlib
def get_content_hash(content):
"""計算內容的雜湊值"""
return hashlib.md5(content.encode('utf-8')).hexdigest()
# 使用範例:找出重複的文章
articles = [
"這是第一篇文章的內容...",
"這是第二篇文章的內容...",
"這是第一篇文章的內容...", # 重複!
]
seen_hashes = set()
unique_articles = []
for article in articles:
article_hash = get_content_hash(article)
if article_hash not in seen_hashes:
seen_hashes.add(article_hash)
unique_articles.append(article)
else:
print(f"發現重複文章(雜湊: {article_hash[:8]}...)")
print(f"原本 {len(articles)} 篇,去重後 {len(unique_articles)} 篇")常見問題與注意事項
Q1: 為什麼 hash() 的結果每次執行 Python 都不同?
從 Python 3.3 開始,為了防止「雜湊碰撞攻擊」(Hash DoS Attack),Python 對字串的雜湊加入了隨機種子。
# 第一次執行
hash("hello") # 可能是 8765588966450260
# 重新啟動 Python 後
hash("hello") # 可能變成 -4567890123456789如果需要跨執行期間一致的雜湊值,請使用 hashlib。
Q2: 雜湊碰撞是什麼?
當兩個不同的輸入產生相同的雜湊值時,就發生了「碰撞」。
# 理論上可能發生(很罕見)
hash("value_a") == hash("value_b") # 不同輸入,相同輸出好的雜湊函數會盡量減少碰撞,但無法完全避免(因為輸入是無限的,輸出是有限的)。
Q3: 什麼時候用 hash()?什麼時候用 hashlib?
| 場景 | 使用 |
|---|---|
| 把物件放進 dict 或 set | hash() |
| 儲存/驗證密碼 | hashlib(配合加鹽) |
| 驗證檔案完整性 | hashlib |
| 產生唯一識別碼 | hashlib |
| 需要跨平台一致的結果 | hashlib |
Q4: 元組可以雜湊,但如果元組裡面有列表呢?
元組本身是不可變的,但如果它包含可變物件,就無法雜湊:
# ✅ 元組內都是不可變物件
hash((1, 2, "hello")) # 可以
# ❌ 元組內包含列表
hash((1, 2, [3, 4])) # TypeError: unhashable type: 'list'總結
兩種雜湊,完全不同!
hash() 資料結構雜湊 | hashlib 密碼學雜湊 | |
|---|---|---|
| 目的 | 讓 dict/set 查找變快 | 保護資料安全 |
| 場景 | 字典鍵、集合成員 | 密碼儲存、檔案驗證 |
| 需要不可逆? | ❌ 不需要 | ✅ 需要 |
| 需要雪崩效應? | ❌ 不需要 | ✅ 需要 |
| 跨執行一致? | ❌ 不一致 | ✅ 一致 |
| 跟「加密」有關? | ❌ 完全無關 | ✅ 有關 |
核心要點回顧
- Python 的「雜湊」有兩種:資料結構用的
hash()和安全用的hashlib hash()跟加密無關:它只是讓 dict 和 set 跑得快- 只有不可變物件能用
hash():list、dict、set 不能當字典的鍵 - 密碼學雜湊才有那些「加密特性」:不可逆、雪崩效應等
- 自定義類別:需要同時實作
__hash__()和__eq__()