Python 雜湊(Hashing)詳解:新手指南

Published October 3, 2024 by 徐培鈞
Python

你可能聽過「雜湊」這個詞,並且覺得它跟「加密」是同一回事——這是一個非常常見的誤解!

在 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()資料結構(dict, set)
hashlib安全應用(密碼、驗證)
hash()Python 整數
hashlib十六進位字串
hash()❌ 每次執行可能不同
hashlib✅ 永遠一致
hash()低(不適合安全用途)
hashlib高(密碼學等級)
hash()Python 內建
hashlibMD5, 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) 和專門的密碼雜湊函數如 bcryptargon2,以防止彩虹表攻擊。

應用二:檔案完整性驗證

確認下載的檔案沒有被篡改。

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

使用hash()
使用hashlib(配合加鹽)
使用hashlib
使用hashlib
使用hashlib

Q4: 元組可以雜湊,但如果元組裡面有列表呢?

元組本身是不可變的,但如果它包含可變物件,就無法雜湊:

# ✅ 元組內都是不可變物件
hash((1, 2, "hello"))  # 可以

# ❌ 元組內包含列表
hash((1, 2, [3, 4]))  # TypeError: unhashable type: 'list'

總結

兩種雜湊,完全不同!

hash() 資料結構雜湊讓 dict/set 查找變快
hashlib 密碼學雜湊保護資料安全
hash() 資料結構雜湊字典鍵、集合成員
hashlib 密碼學雜湊密碼儲存、檔案驗證
hash() 資料結構雜湊❌ 不需要
hashlib 密碼學雜湊✅ 需要
hash() 資料結構雜湊❌ 不需要
hashlib 密碼學雜湊✅ 需要
hash() 資料結構雜湊❌ 不一致
hashlib 密碼學雜湊✅ 一致
hash() 資料結構雜湊❌ 完全無關
hashlib 密碼學雜湊✅ 有關

核心要點回顧

  1. Python 的「雜湊」有兩種:資料結構用的 hash() 和安全用的 hashlib
  2. hash() 跟加密無關:它只是讓 dict 和 set 跑得快
  3. 只有不可變物件能用 hash():list、dict、set 不能當字典的鍵
  4. 密碼學雜湊才有那些「加密特性」:不可逆、雪崩效應等
  5. 自定義類別:需要同時實作 __hash__()__eq__()