HTMX 是一個輕量級的前端框架,允許開發者直接在 HTML 中使用屬性來實現動態交互,無需撰寫大量的 JavaScript。它支持使用標準的 HTTP 方法(GET、POST、PUT、DELETE)進行局部更新,並且可以與 Django 等後端框架無縫整合。 HTMX 的核心功能 1. 局部更新 HTMX 可以通過 hx-get 或 hx-post 等屬性發送請求,並將返回的 HTML 片段插入到指定的 DOM 節點中。 範例:按讚功能 {% if user in interview.favorited_by.all %} 已收藏 {% else %} 收藏 {% endif %} hx-post: 發送 POST 請求到指定的 URL。 hx-swap="outerHTML": 替換按鈕本身的 HTML,而不僅僅是其內部內容。 2. 動態載入內容 HTMX 支持在用戶操作時動態載入內容,例如模態視窗(Modal)。 範例:動態載入模態視窗 <button hx-get="/modal" hx-target="#modal-container" class="btn">打開模態視窗</button> <div id="modal-container"></div> hx-get: 發送 GET 請求到 /modal。 hx-target: 指定將返回的 HTML 插入到 #modal-container 中。 3. 表單提交 HTMX 可以用於處理表單提交,並動態更新頁面的一部分。 範例:新增留言 <form action="{% url 'interviews:comment' interview.id %}" method="post" hx-target=".list" hx-swap="beforeend"> {% csrf_token %} <textarea name="content"></textarea> <button>新增留言</button> </form> hx-target: 指定將返回的內容插入到 .list 元素中。 hx-swap="beforeend": 將返回的內容追加到 .list 的末尾。 4. 事件處理 HTMX 支持使用屬性來處理事件,例如在請求完成後執行操作。 範例:顯示成功訊息 <button hx-post="/action" hx-trigger="click" hx-on="htmx:afterRequest: alert('操作成功')">執行操作</button> hx-trigger: 指定觸發請求的事件。 hx-on: 綁定 HTMX 事件(如 htmx:afterRequest)並執行 JavaScript。 HTMX 與 Django 的整合 1. 後端返回部分模板 在 Django 視圖中,返回一部分模板作為 HTMX 的響應。 範例:按讚視圖 views.py @require_POST @login_required def favorite(req, id): interview = get_object_or_404(Interview, pk=id) favorites = req.user.favorite_interviews if favorites.filter(pk=interview.pk).exists(): favorites.remove(interview) else: favorites.add(interview) return render(req, "interviews/favorite.html", {"user": req.user, "interview": interview}) favorite.html {% if user in interview.favorited_by.all %} 已收藏 {% else %} 收藏 {% endif %} 視圖返回的模板只包含按鈕的 HTML,HTMX 會自動更新頁面。 2. CSRF Token HTMX 請求需要包含 CSRF Token,否則 Django 會拒絕請求。 解決方法 在 HTML 的 <body> 標籤中添加以下屬性: <body hx-headers='{"x-csrftoken": "{{ csrf_token }}"}'> 3. 局部渲染 HTMX 可以用於實現局部渲染,避免整頁刷新。 範例:留言列表 <form action="{% url 'interviews:comment' interview.id %}" method="post" hx-target=".list" hx-swap="beforeend"> {% csrf_token %} <textarea name="content"></textarea> <button>新增留言</button> </form> <ul class="list"> {% for comment in comments %} {{ comment.user }} 說:{{ comment.content }} {% endfor %} </ul> 新增留言後,HTMX 只更新 .list 的內容,而不是整個頁面。 HTMX 的優勢 簡化前端開發: 無需撰寫大量 JavaScript,直接在 HTML 中定義交互邏輯。 與後端無縫整合: 使用 Django 模板引擎生成 HTML,減少前後端分離的複雜性。 提升性能: 支持局部更新,減少不必要的頁面刷新。 HTMX 的限制 複雜交互: 對於需要大量前端邏輯的應用,HTMX 可能不如前端框架(如 React 或 Vue)靈活。 學習曲線: 雖然 HTMX 簡單,但需要熟悉其屬性和事件。 總結 HTMX 是一個強大且輕量的工具,適合用於需要快速開發的 Django 項目。通過 HTMX,可以實現高效的局部更新和動態交互,提升用戶體驗,同時保持代碼的簡潔性。

在本筆記中,將探討如何在 Django 中處理「動態切換按讚狀態」的邏輯: 介紹 ManyToMany 關聯模型的建立,並解釋如何使用它來設置使用者 (User) 與面試(Interview)之間的關聯。 說明為何在這樣的情境下,不能使用 get_object_or_404(),而是應該使用 filter() 方法來處理查詢。 展示如何在 favorite 這樣的 view function 中實現按讚與取消按讚功能,動態切換按讚狀態。 ManyToMany 關聯模型的建立 在 Django 中,使用 ManyToMany 關聯來表示多對多的關係。假設有 User 和 Interview 模型,可以建立以下的多對多關聯: interviews/models.py from django.contrib.auth.models import User from django.db import models class Interview(models.Model): # 其他欄位... user = models.ForeignKey(User, on_delete=models.CASCADE) favorited_by = models.ManyToManyField( User, through="FavoriteInterview", related_name="favorite_interviews", # join 欄位 ) # join table class FavoriteInterview(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) interview = models.ForeignKey(Interview, on_delete=models.CASCADE) 關聯設置 FavoriteInterview 表示每個使用者對於某個面試的「按讚」記錄。每一筆記錄由 user 和 interview 兩個外鍵組成。 使用 ManyToManyField 假設希望在 User 模型中表示使用者按讚過的所有 Interview,可以在 Interview 模型中設置 ManyToManyField 來表示這種關聯 通過 interview.favorited_by 來獲取按讚某篇面試的所有使用者 通過 user.favorite_interviews 來獲取某位使用者按讚的所有面試 在 favorite view function 中處理「按讚」和「取消按讚」的邏輯 interviews/views.py from django.shortcuts import get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST @require_POST @login_required def favorite(req, id): interview = get_object_or_404(Interview, pk=id) # 獲取指定的 interview favorites = req.user.favorite_interviews # 登入的 user 按讚的所有面試 # 以 User model 的角度處理 ManyToMany 關聯 # 判斷這位使用者是否已經按過讚 if favorites.filter(pk=interview.pk).exists(): # 如果有按過讚,則取消按讚(remove),在 FavoriteInterview table 裡刪除該筆資料 favorites.remove(interview) else: # 如果沒有按過讚,則加上(add),在 FavoriteInterview table 裡增加該筆資料 favorites.add(interview) return redirect("interviews:show", id=interview.id) 功能解釋: get_object_or_404():查找指定的 Interview 實例,若找不到則會拋出 404 錯誤(這裡是確保該面談存在)。 filter(pk=interview.pk).exists():檢查該 user 是否已經按讚過這篇 interview,如果已經按讚,則執行 remove();如果沒有按讚,則執行 add(),這樣就能夠動態切換按讚狀態。 redirect():操作完成後,重定向回該 interview 的詳細頁面。 小結 ManyToMany 關聯的建立:使用 ManyToManyField 和 through、related_name 參數來設置 User 和 Interview 之間的多對多關聯,並且在中介模型 FavoriteInterview 中存儲按讚資料。 使用 filter() 判斷使用者是否已經按過讚:使用 filter() 和 .exists() 來檢查 FavoriteInterview 是否存在對應的關聯,根據結果決定是按讚還是移除按讚。 filter() 方法可以返回符合條件的查詢結果,如果沒有找到任何符合條件的資料,它會返回一個空的 queryset,而不是拋出錯誤,因此filter() 可以靈活處理按讚的狀態 為何 get_object_or_404() 不適用判斷使用者是否已經按過讚:在動態切換按讚狀態時,使用 get_object_or_404() 不適合,因為查詢結果沒有找到資料,它會拋出 404 錯誤,這會讓我們無法靈活處理「已按讚」和「未按讚」的情況。 從不同 Model 角度處理 ManyToMany 關聯 在 Django 中,ManyToMany 關聯允許兩個模型之間建立多對多的關聯。這些關聯通常由 ManyToManyField 或透過額外的關聯表(如 through 設定)來實現。在不同的 Model 角度處理 ManyToMany 關聯時,我們有不同的寫法來處理關聯資料的新增、刪除與查詢。 1. 從 User 模型的角度處理 ManyToMany 關聯 首先,讓我們回顧如何從 User 模型的角度處理 ManyToMany 關聯。在這個例子中,User 和 Interview 之間的關聯是透過 favorite_interviews(即 ManyToManyField)來建立的。我們可以在 User 模型上使用 favorite_interviews,來處理這篇 Interview 是否已經被使用者按讚過。 @require_POST @login_required def favorite(req, id): interview = get_object_or_404(Interview, pk=id) user = req.user # 以 User model 的角度處理 ManyToMany 關聯 # 判斷這位使用者是否已經按過讚 if user.favorite_interviews.filter(pk=interview.pk).exists(): # 如果有按過讚,則取消按讚(remove) user.favorite_interviews.remove(interview) else: # 如果沒有按過讚,則加上(add) user.favorite_interviews.add(interview) user.favorite_interviews.filter(pk=interview.pk):我們通過 filter 查詢使用者是否已經對這篇 interview 按讚過,這會返回一個 QuerySet,並且使用 exists() 方法檢查是否存在此條記錄。 remove() 和 add():remove() 用來移除使用者對該 interview 的按讚記錄,add() 用來新增按讚記錄。 2. 從 Interview 模型的角度處理 ManyToMany 關聯 另一種處理方式是從 Interview 模型的角度來管理 ManyToMany 關聯。可以直接操作 favorited_by,這是 Interview 模型中的 ManyToManyField,它用來表示所有按讚過這篇訪談的使用者。 # 以 Interview model 的角度處理 ManyToMany 關聯 # 判斷這篇文章是否已經被按過讚 if interview.favorited_by.filter(pk=user.pk).exists(): # 如果有按過讚,則取消按讚(remove) interview.favorited_by.remove(user) else: # 如果沒有按過讚,則加上(add) interview.favorited_by.add(user) interview.favorited_by.filter(pk=user.pk):使用 filter 查詢是否該 user 已經按讚過這篇 interview,如果有,則移除按讚;如果沒有,則將按讚關聯添加回 favorited_by。 3. 從 FavoriteInterview 模型的角度處理 ManyToMany 關聯 如果我們使用的是 through 設定的 ManyToMany 關聯(在本例中,使用了 FavoriteInterview 作為關聯模型),可以直接操作 FavoriteInterview 模型來添加或刪除按讚記錄。 # 以 FavoriteInterview model 的角度處理 ManyToMany 關聯 favorite_interview = FavoriteInterview.objects.filter(user=user, interview=interview) if favorite_interview.exists(): # 如果已經按過讚,則刪除該記錄 favorite_interview.delete() else: # 如果沒有按過讚,則新增該記錄 FavoriteInterview.objects.create(user=user, interview=interview) 首先查詢是否已經存在這個 FavoriteInterview 記錄,如果存在則刪除(使用 .delete()),如果不存在則創建新的 FavoriteInterview 記錄(使用 .create())。 使用 filter(user=user, interview=interview) 來確保我們只關注到該使用者和該訪談的關聯。 總結: ManyToManyField 在 Django 可以用來管理多對多的關聯。但當需要處理這些關聯時,必須明確知道該如何從不同的角度來操作。 從 User 模型的角度:我們可以操作該使用者的 favorite_interviews 屬性來添加或移除關聯。 從 Interview 模型的角度:我們可以操作該面試的 favorited_by 屬性來處理關聯。 從 FavoriteInterview 模型的角度:透過 through 設定的關聯表,可以直接操作這個模型來進行新增或刪除。 這些方法各有其使用場景,選擇哪一種方式取決於你的需求和數據模型的設計。在處理 ManyToMany 關聯時,要特別注意數據一致性和簡潔性,並選擇最符合需求的解決方案。

這篇筆記說明如何用 Git Subtree 設計一個在 GitHub 上「可重複練習、集中管理」的多分支專案架構,並透過 Makefile 實現自動化整合。 核心目標是: 從 template 分支快速建立開發練習分支,讓每次開發練習都從一致的起點開始 每個開發分支獨立開發、保有完整 commit 紀錄 最終成果自動整合到 master 分支的子資料夾,方便集中展示與維運管理 這樣的流程設計有以下優點: 快速複製練習環境,便於反覆練習與教學 每次練習皆為獨立專案,保留完整開發歷程 成果集中到 master,只需瀏覽一個分支即可看到所有進度 可搭配 GitHub Actions 等工具,自動化整合與部署流程 🧱 建立 template 分支 template 分支是所有練習的起始樣板,可以依照以下兩種方式建立: 建立初始開發環境:準備好基礎的資料夾架構、依賴設定檔(例如 package.json、requirements.txt)或共用的 utility function。 給予練習規格:可以只放入說明文件、範例輸入/輸出或 TODO 任務,讓想練習的人依據這份規格自行開發。 建立指令如下: # 從 master 建立 template 分支 git checkout -b template # 推送到遠端 git push origin template ⚙️ 更改預設分支(GitHub 操作) 前往 GitHub Repo 頁面 點擊「⚙ Settings」 左側選單中選擇「Branches」 將 Default branch 改為 template 這個設定是為了能夠先刪除 master 分支並重建,讓 master 存放練習後的成果 🗑️ 刪除 master 並重建 如果你想讓 master 分支只存放整合後的成果,而非開發內容,可以重新建立一個乾淨的 master 分支: # 刪除遠端 master 分支 git push origin --delete master # 本地切換並建立新的空 master 分支 git checkout --orphan master # 移除所有檔案(保留工作區) git rm -rf . # 建立 .gitignore 和 Makefile touch .gitignore Makefile # 提交並推送新的空白 master 分支 git add .gitignore Makefile git commit -m "Initialize new master branch" git push -u origin master ⚠️ 請記得將預設分支從 template 改回 master,以便未來集中展示成果與管理。 用 git subtree 將多分支專案整合 git subtree 是 Git 提供的工具,允許你將另一個分支的內容匯入到當前分支的某個子資料夾,並保留其完整的 commit 歷史。 git subtree add --prefix=<資料夾路徑> <遠端名稱>/<分支名稱> 這條指令的作用是將指定的遠端分支內容匯入到當前分支的某個子資料夾中,並保留該分支的完整 commit 歷史 --prefix=<資料夾路徑>: 指定將分支內容匯入到當前分支的哪個子資料夾。 如果該資料夾不存在,Git 會自動建立。 <遠端名稱>/<分支名稱>: 指定要匯入的遠端分支。 例如,origin/feature-1 表示從遠端 origin 的 feature-1 分支匯入。 在本專案中,我們使用 git subtree 的目的包括: 將每個練習分支(如 feature_1)的專案內容,整合匯入到 master 分支的對應子資料夾(如 feature_1/) 避免用 merge 或 cherry-pick 導致 commit 歷史混亂 可重複執行,不會影響其他資料夾內容 建立 Makefile 自動遷移腳本 以下是 master 分支根目錄的 Makefile 實作: .PHONY: archive archive-all # 將指定分支內容以 git subtree 匯入對應資料夾,保留完整 commit 紀錄 archive: ifndef name $(error Usage: make archive name=feature-<X>) endif @echo "🌲 Archiving $(name) into master:$(name)/ using git subtree..." # 確保在 master 分支 @git checkout master # 檢查有無未提交的變更 @if [ -n "$$\(git status --porcelain)" ]; then \ echo "❌ master branch has uncommitted changes. Please commit or stash first."; \ exit 1; \ fi # 匯入分支內容到對應資料夾(不使用 --squash,可保留所有 commit) @git subtree add --prefix=$(name) origin/$(name) @echo "✅ Done archiving $(name) into master:$(name)/" # 批次對所有符合 feature-* 的分支執行 subtree 匯入 archive-all: @git fetch origin @git branch -r | grep 'origin/feature_' | sed 's|origin/||' | while read branch; do \ $(MAKE) archive name=$$branch; \ done 📦 歸檔練習分支內容 每完成一個 feature_<n> 分支的專案後,執行: make archive name=feature_<n> 系統會自動將該分支所有檔案以 git subtree 匯入到 master 的 feature_<n>/ 資料夾中,保留完整 commit 歷史。 若要一鍵歸檔所有 feature_<n> 分支,可執行: make archive-all 🚀 後續可整合 GitHub Actions 自動化流程 未來可以使用 GitHub Actions 自動觸發 make archive name=feature_<n> 指令,將 PR 合併後的分支內容自動歸檔到 master 的對應子資料夾。建議流程如下: 當 PR 合併至 feature_<n> 分支時觸發 workflow 使用 actions/checkout 切換到 master 分支 使用 git fetch 確保能抓到對應的分支內容 執行 make archive name=feature_<n> 自動歸檔 將更新過的 master 分支 push 回遠端 這樣可以避免手動切換與操作,讓每個練習成果在合併 PR 後自動整理到展示區。你也可以加入條件判斷,只對分支名稱符合 feature_* 格式的情況執行此流程。 ✅ 結果展示 master ├── .gitignore ├── Makefile ├── feature_1/ │ ├── manage.py │ ├── app/ │ └── ... ├── feature_2/ │ └── ... └── feature_3/ └── ... 這樣的架構讓你可以: 保有一份可重複使用的範本分支(template) 在多個練習分支開發而不互相干擾 將成果統一集中到 master 展示,方便他人瀏覽 將 CI/CD、部屬、技術分享等操作聚焦於 master 管理 非常適合用於建立個人練習倉庫、技術文章範例、或團隊協作練習環境。

1. Git 的核心概念 Git 的版本控制就像拍照一樣,能夠記錄當下的專案狀態(snapshot),並允許我們回到某個時間點的狀態。 版本控制:每次提交(commit)都像拍照,記錄當下的檔案狀態。 非破壞性操作:Git 的操作不會刪除歷史,只會新增記錄,讓我們可以隨時回溯。 2. 安裝 Git 在 macOS 上可以使用 Homebrew 來安裝 Git: brew install git git --version 3. 初始化專案 初始化一個 Git 儲存庫,會在專案目錄中建立 .git 資料夾: git init 4. 基本操作 查看狀態 檢查目前的檔案狀態: git status 追蹤檔案 將檔案加入暫存區: git add index.html 取消追蹤檔案 將檔案從暫存區移回工作目錄: git restore --staged index.html 提交變更 提交檔案並附加說明訊息: git commit -m "feat: add index.html" 5. git config 設定使用者資訊 設定全域的使用者名稱與信箱: git config --global user.name 'YOURNAME' git config --global user.email 'YOUR_EMAIL' 查看目前的 Git 設定: git config --list 6. 提交訊息的最佳實踐 避免使用模糊的訊息,例如: "update" "bug fixed" "temp" 推薦使用具體且有意義的訊息,例如: "#23 bug fixed" "member sign in (WIP)" 建議遵循 Conventional Commits 標準。 7. git log 查看歷史紀錄 使用指令 查看專案的提交歷史: git log 使用工具 在 VS Code 中可以使用 Git Graph 插件來視覺化歷史紀錄。 8. git restore 還原檔案 還原單一檔案 將檔案還原到最新一次的提交狀態: git restore index.html 還原所有檔案 將整個目錄還原到最新一次的提交狀態(不推薦): git restore . 9. git branch 分支操作 查看分支 列出所有分支: git branch 建立分支 建立新分支: git branch cat 刪除分支 刪除分支: git branch -d git branch -d 用於刪除 已經合併到當前分支的分支。如果分支尚未合併,Git 會拒絕刪除,並提示錯誤,避免意外刪除未合併的工作。 git branch -d <branch_name> git branch -D git branch -D 是強制刪除分支的指令,無論分支是否已經合併到當前分支,都會直接刪除。 不需要檢查分支是否已合併。 適用於確定不需要該分支的情況,但需謹慎使用,因為未合併的工作可能會丟失(除非透過 git reflog 找回)。 找回被刪除的分支:如果誤刪分支,可以使用 git reflog 找回: git reflog git branch <branch_name> <commit_id> 切換分支 切換到指定分支: git switch cat 10. git merge 合併分支 快轉 (fast-forward) 合併 將目前分支快轉到另一個分支: git switch master git merge cat 合併前的 Git Graph: * C3 (cat) | * C2 (master) | * C1 合併後(快轉合併): * C3 (cat, master) | * C2 | * C1 非快轉合併 如果想保留合併歷程,產生一個新的合併節點(merge commit),可以使用 --no-ff 參數: git merge cat --no-ff -m "merge" 合併後(非快轉合併): * M (master) merge commit |\ | * C3 (cat) | | * | C2 |/ * C1 M(merge commit):新的合併節點,記錄了 dog 和 cat 分支的合併歷史。 分支歷史:dog 分支的歷史現在包含了 cat 分支的提交。 合併歷程可視化:這種方式保留了分支的合併歷史,能在 Git Graph 中清楚地看到分支的合併過程。 適用場景:當需要保留分支的合併歷史,或在團隊協作中需要清楚記錄分支的來源時,使用非快轉合併是更好的選擇。 無法快轉的合併 在 Git 中,快轉合併(fast-forward merge)是指當目標分支的歷史是當前分支的直接延續時,Git 只需將當前分支的指標移動到目標分支的最新提交即可完成合併。 然而,當兩個分支的歷史路徑不同時,無法進行快轉合併,因為這兩個分支的提交歷史已經分叉。此時,Git 需要創建一個新的合併節點來將這兩條歷史路徑合併在一起。 當兩個分支的歷史路徑不同時,無法進行快轉合併(fast-forward merge),這種情況會產生一個新的合併節點(merge commit)。以下是詳細的解釋與範例。 git switch dog git merge cat -m "Merge branch 'cat' into dog" 與快轉合併的比較 特性 快轉合併(Fast-Forward Merge) 無法快轉合併(Non-Fast-Forward Merge) 提交歷史 不產生新的合併節點 產生新的合併節點 分支歷史 無法保留分支的獨立歷史 保留分支的獨立歷史 適用場景 單人開發或簡單的分支合併 團隊協作或需要完整歷史記錄 11 Merge 衝突未解決的情況 當執行 git merge 時,如果發生衝突但未解決,Git 仍然允許合併完成,但這可能導致合併的內容不正確。 解決方法 檢查衝突:Git 會在衝突的檔案中標註衝突的區域,需要手動編輯檔案來解決衝突。 提交修正後的結果:git add . git commit -m "Resolve merge conflict" 如果決定放棄合併,可以使用以下指令取消合併過程:git merge --abort 12. git rebase 用於重新整理分支的提交歷史。它可以將一個分支的基底(base)換到另一個分支上,從而使提交歷史更加線性化,便於閱讀和管理。 git rebase 其實算是一個「危險」的指令,因為它會改寫整個commit 的歷史,實務上不會對團隊共同開發的穩定分支(像master, main, dev,…等)下這個指令 Rebase 的用途 清理提交歷史:將分支的提交歷史整理成一條直線,避免分支間的交錯。 整合功能分支:在將功能分支合併到主分支之前,使用 Rebase 將功能分支的基底更新到最新的主分支。 避免多餘的合併節點:與 merge 不同,Rebase 不會產生新的合併節點(merge commit)。 Rebase 的基本操作 以下是將 dog 分支的基底換到 cat 分支的範例: git switch dog git rebase cat 執行上述指令後,dog 分支的提交歷史會被重新整理,並接在 cat 分支的最新提交之後。 Rebase 的優缺點 優點: 簡化歷史:提交歷史更加線性,便於閱讀。 避免多餘的合併節點:不像 merge,Rebase 不會產生新的合併節點。 缺點: 破壞歷史:Rebase 會改變提交的哈希值(commit hash),因此不適合用於已經共享的分支。 衝突處理:如果 Rebase 過程中發生衝突,需要手動解決,並使用以下指令繼續 Rebase:git rebase --continue Rebase 與 Merge 的比較 特性 Rebase Merge 提交歷史 線性化,歷史更簡潔 保留完整的分支合併歷史 合併節點 不產生新的合併節點 產生新的合併節點(merge commit) 適用場景 清理歷史、更新基底 保留分支歷史 衝突處理 每個提交單獨解決衝突 一次解決所有衝突 注意事項 避免在共享分支上使用 Rebase:Rebase 會改變提交的哈希值,可能導致其他開發者的分支出現問題。 記住原始狀態: git rebase 是一個「危險」的指令,rebase 前,git 會自動保存原始狀態到 ORIG_HEAD,如果需要回到 rebase 前的狀態,可以使用以下指令: git reset ORIG_HEAD --hard 13. git reset 刪除與復原節點 刪除末端節點 將目前的分支重置到指定的提交: git reset <ID> 重置模式: --mixed(預設):異動會出現在工作目錄(unstaged)。 --soft:異動會出現在 staging 區(已 add)。 --hard:異動會直接被丟棄。 使用 git reflog 找回節點 git reflog 紀錄 HEAD 的移動 如果分支被刪除,該節點會變成無名分支(dangling commit)。可以使用 git reflog 找回並重新貼上分支: git branch cat <commit_id> 14. 進階技巧 查詢檔案歷史 查看檔案每行程式的作者與修改時間: git blame index.html 在 VS Code 中可以使用 GitLens 插件直接查看。 修改分支名稱 重新命名分支: git branch -m new_name git branch 也可能造成 HEAD 移動: 改 branch 名字, HEAD 本身沒有動,但換指向另一個地方,reflog 有紀錄 15. 復原操作 復原 Merge git merge cat -m "Merge" git reset HEAD^ --hard # 回到 HEAD 的上一步 復原 Rebase git rebase cat git reset ORIG_HEAD --hard Rebase 前會自動存一份 ORIG_HEAD,方便回到舊狀態。 16. Reset 與 Reflog 的搭配使用 在 Git 中,reset 和 reflog 是一組強大的工具,能夠讓我們靈活地操作分支歷史: Reset:可以將目前的分支移動到任何指定的節點,讓分支的狀態回到該節點的樣子。這就像「飛行」一樣,可以快速跳轉到不同的提交。 Reflog:記錄了 HEAD 的所有移動歷史,就像一張「地圖」,幫助我們追蹤分支的變化。即使某些節點不再被分支指向,也可以透過 reflog 找回。 使用範例 查看 HEAD 的移動歷史: git reflog 將分支重置到某個歷史節點: git reset <commit_id> --hard 注意事項 reset 是一個強大的工具,但需謹慎使用,特別是 --hard 模式,因為它會丟棄未提交的變更。 reflog 是找回遺失節點的救命工具,即使分支被刪除,節點仍然可以透過 reflog 找回。 透過 reset 和 reflog 的搭配,可以靈活地操作分支歷史,並在需要時回到任何過去的狀態。 17. 隱形節點的產生 在 Git 中,每個分支都像一個便利貼,指向某個提交(commit)。當分支被刪除後,該分支指向的節點(末端節點)會變成無人看管的狀態,這些節點會「隱形」,但實際上它們仍然存在於 Git 的歷史中。 讓隱形節點重新現形 如果需要讓隱形的節點重新出現在分支中,可以將分支重新貼回該節點: 使用 git reflog 找到隱形節點的提交 ID。git reflog 建立一個新的分支,指向該提交:git branch <branch_name> <commit_id> 18. Git 的非破壞性操作 Git 的操作本質上是非破壞性的,所有的提交歷史都會被保留。即使使用 git rebase,原始的節點也不會被刪除,而是被隱藏。 Rebase 的背後原理 複製節點:git rebase 的操作實際上是複製原始節點,並將它們移動到新的基底(base)後面。 隱藏原始節點:原始的節點會被隱藏,但可以透過 git reflog 找回。 19. 結語 Git 是一個功能強大的版本控制工具,熟練掌握後可以大幅提升開發效率與團隊協作能力。

以下是安裝 PostgreSQL 以及基本使用的完整流程與注意事項。 1. 安裝 PostgreSQL 使用 Homebrew 安裝指定版本的 PostgreSQL: brew install postgresql@17 確認安裝路徑: brew --prefix postgresql # 範例輸出:/usr/local/opt/postgresql@17 將 PostgreSQL 的執行檔加入環境變數: export PATH="/usr/local/opt/postgresql@17/bin:$PATH" >> ~/.zshrc source ~/.zshrc 確認安裝是否成功: psql --version 2. 啟動 PostgreSQL 服務 啟動 PostgreSQL: brew services start postgresql@17 若啟動失敗,可能需要初始化資料庫: initdb -D /usr/local/var/postgresql@17 brew services restart postgresql@17 檢查 PostgreSQL 狀態: pg_ctl -D /usr/local/var/postgresql@17 status 3. 建立使用者與資料庫 建立預設的 postgres 使用者與資料庫: createuser -s postgres createdb -U postgres postgres 4. 使用 psql 連線到資料庫 連線到 PostgreSQL: psql -h localhost -U postgres -d postgres 進入 psql 後,建立新的資料庫: CREATE DATABASE testdb; 切換到新建立的資料庫: \c testdb 建立資料表範例: CREATE TABLE user ( id UUID PRIMARY KEY, name TEXT, birthday DATE ); 5. 檢查 PostgreSQL 狀態的自動化腳本 建立一個檢查 PostgreSQL 狀態的腳本 check_postgres.sh: #!/bin/bash PGDATA="/usr/local/var/postgresql@17" echo "🔍 Checking PostgreSQL status..." pg_ctl -D "$PGDATA" status if [ $? -ne 0 ]; then echo "🚀 PostgreSQL not running, attempting to start..." pg_ctl -D "$PGDATA" start else echo "✅ PostgreSQL already running." fi echo "🌐 Checking port 5432..." lsof -iTCP:5432 -sTCP:LISTEN || echo "❌ Not listening on port 5432" echo "📋 Existing databases:" psql -l || echo "⚠️ psql failed. Is PostgreSQL running?" 執行腳本檢查 PostgreSQL 狀態: bash check_postgres.sh 6. 常見問題與解決方法 問題 1:psql: 錯誤: role "postgres" does not exist 解決方法: 使用 createuser -s postgres 建立 postgres 使用者。 問題 2:啟動 PostgreSQL 時出現 Bootstrap failed: 5: Input/output error 解決方法: 刪除舊的啟動檔案並重新啟動服務:rm ~/Library/LaunchAgents/homebrew.mxcl.postgresql@17.plist brew services cleanup brew services start postgresql@17 7. 注意事項 版本管理:安裝多個版本的 PostgreSQL 時,需確認環境變數是否正確指向目標版本。 資料庫初始化:若安裝後無法啟動,可能需要手動執行 initdb 初始化資料庫。 權限問題:確保使用者擁有足夠的權限來操作資料庫。 服務管理:使用 brew services 管理 PostgreSQL 的啟動與停止。

登入檢查:login_required 原理與機制 Django 提供了 @login_required 裝飾器,用於保護視圖,確保只有已登入的用戶才能訪問特定的頁面。當未登入的用戶嘗試訪問受保護的視圖時,Django 會自動將用戶重定向到設定中的 LOGIN_URL,並附加一個 next 查詢參數 (QueryString),表示用戶原本想訪問的 URL。 使用範例 from django.contrib.auth.decorators import login_required from django.shortcuts import render @login_required def protected_view(req): return render(req, "protected_page.html") 工作原理 當用戶訪問被 @login_required 裝飾的視圖時,Django 會檢查 req.user.is_authenticated 的值。 如果 req.user.is_authenticated 為 True,則允許訪問該視圖。 如果 req.user.is_authenticated 為 False,則會將用戶重定向到 settings.LOGIN_URL,並附加 next 查詢參數,指向用戶原本想訪問的 URL。 關鍵點 req.user.is_authenticated 是一個屬性,用於判斷當前用戶是否已登入。 如果用戶已登入,req.user 是一個 User 對象,且 is_authenticated 為 True。 如果用戶未登入,req.user 是一個 AnonymousUser 對象,且 is_authenticated 為 False。 相關代碼(簡化版) @login_required 的內部邏輯類似於以下代碼: from django.shortcuts import redirect from django.conf import settings def login_required(view_func): def wrapper(req, *args, **kwargs): if req.user.is_authenticated: return view_func(req, *args, **kwargs) else: login_url = settings.LOGIN_URL return redirect(f"{login_url}?next={req.path}") return wrapper @login_required 的授權條件是基於 req.user.is_authenticated 的值。如果你需要自定義授權邏輯,可以考慮使用 Django 的 PermissionRequiredMixin 或自定義裝飾器。 工作流程 用戶訪問受保護的視圖。 如果未登入,Django 會將用戶重定向到 LOGIN_URL,例如:/users/sign_in?next=/protected_page Django 不會自動處理 next 的重定向邏輯,需要在登入處理的視圖中手動處理 next 的值,並根據它進行重定向。 如何處理 next 原理與機制 next 是一個查詢參數,用於保存用戶在未登入時嘗試訪問的目標頁面。當用戶成功登入後,應該根據 next 的值將用戶重定向到該頁面。 1. 在登入表單中傳遞 next 在登入頁面中,將 next 的值作為隱藏字段傳遞到表單中,這樣用戶提交表單時,next 的值會被一併提交。 sign_in.html <form action="{% url 'users:create_session' %}" method="post"> {% csrf_token %} <input type="hidden" name="next" value="{{ next }}" /> <input type="text" name="username" placeholder="Username" /> <input type="password" name="password" placeholder="Password" /> <button type="submit">登入</button> </form> 2. 在後端處理 next 在處理登入的視圖中,檢查 req.POST 或 req.GET 中是否有 next 的值,並在登入成功後進行重定向。 users/views.py from django.contrib.auth import authenticate, login from django.shortcuts import redirect def sign_in(req): next = req.GET.get("next", reverse("pages:index")) return render(req, "users/sign_in.html", {"next": next}) def create_session(req): username = req.POST['username'] password = req.POST['password'] user = authenticate(username=username, password=password) if user is not None: login(req, user) next = req.POST.get("next", reverse("pages:index")) return redirect(next) else: return redirect("users:sign_in") 工作流程 未登入用戶訪問受保護頁面 用戶被重定向到登入頁面,並附加 next 查詢參數,例如: /users/sign_in?next=/protected_page 用戶提交登入表單 表單中包含 next 的值,後端接收該值。 後端處理重定向 後端檢查 next 的值,並在登入成功後將用戶重定向到該頁面。如果 next 不存在,則重定向到預設頁面(如首頁)。 QueryString 原理與機制 QueryString 是 URL 中的查詢參數部分,用於在 URL 中傳遞數據。格式如下: /path/to/page?key1=value1&key2=value2 在 Django 中,可以使用 req.GET 獲取 QueryString 中的參數。 使用範例 def example_view(req): value = req.GET.get('key', 'default_value') return render(req, "example.html", {"value": value}) 工作流程 用戶訪問包含 QueryString 的 URL,例如:/example?key=hello Django 通過 req.GET 提供訪問 QueryString 的方法。 總結 使用 @login_required 確保視圖只能被已登入用戶訪問。 通過處理 next 查詢參數,實現用戶登入後的重定向。 利用 QueryString 傳遞數據,實現靈活的頁面交互。 使用 logout 方法實現安全的登出功能。 通過外鍵將 Interview 和 Comment 關聯到 User,實現數據的關聯性。 這些功能構成了 Django 中用戶認證與權限管理的基礎,為構建安全、可靠的應用提供了強大的支持。

Django 提供的 login() 函數是用來處理用戶認證並建立會話 (session) 和 cookie 的核心工具。以下是該函數的完整流程及其背後的運作機制。 1. login() 函數的作用 django.contrib.auth.login(request, user) 是用來將已經通過身份驗證的用戶標記為「已登入」的函數。它會執行以下操作: 將用戶的 ID 存儲到 session 中。 更新 session 的相關數據。 設置用戶的身份驗證狀態,讓 request.user 可以用來驗證用戶。 2. 流程詳解 (1) 用戶身份驗證 在調用 login() 函數之前,通常需要先驗證用戶的身份,例如使用 authenticate() 函數: from django.contrib.auth import authenticate, login user = authenticate(request, username='username', password='password') if user is not None: login(request, user) else: # 驗證失敗的處理 authenticate() 函數會檢查用戶名和密碼是否正確,並返回一個用戶對象 (User instance) 或 None。 (2) 調用 login() 函數 當用戶通過身份驗證後,login() 函數會執行以下步驟: 綁定用戶到 session Django 使用 session 框架來管理用戶的登入狀態。 login() 函數會將用戶的主鍵 (Primary Key) 存儲到 session 中,鍵名為 _auth_user_id。 同時,還會存儲用戶的後端信息 (_auth_user_backend),用於標記是哪個身份驗證後端處理了該用戶。 相關代碼片段: request.session[SESSION_KEY] = user.pk # 存儲用戶主鍵 request.session[BACKEND_SESSION_KEY] = user.backend # 存儲後端信息 設置 Cookie Django 的 session 框架會自動將 session ID 存儲到用戶的瀏覽器 Cookie 中。 這個 Cookie 的名稱默認為 sessionid,可以在 settings.SESSION_COOKIE_NAME 中自定義。 當用戶發送後續請求時,瀏覽器會自動攜帶這個 Cookie,Django 會根據 Cookie 中的 session ID 找到對應的 session 數據。 更新 request.user login() 函數會將 request.user 更新為當前登入的用戶對象。 這樣,後續的請求中可以通過 request.user 獲取當前用戶的相關信息。 (3) 驗證用戶狀態 在後續的請求中,Django 會自動驗證用戶的登入狀態: 從 Cookie 中提取 session ID 當用戶發送請求時,Django 會從請求的 Cookie 中提取 session ID,並查找對應的 session 數據。 加載用戶對象 如果 session 中存在 _auth_user_id,Django 會根據該 ID 從數據庫中加載用戶對象,並將其設置為 request.user。 驗證用戶是否已登入 可以通過 request.user.is_authenticated 判斷用戶是否已登入。 3. 完整流程圖 用戶提交登入表單 (包含用戶名和密碼)。 後端調用 authenticate() 驗證用戶身份。 如果驗證成功,調用 login(): 將用戶 ID 存入 session。 設置 session ID 到 Cookie。 更新 request.user。 後續請求中,Django 根據 Cookie 驗證用戶身份。 4. 相關設置 session 存儲方式 Django 默認使用數據庫存儲 session 數據,可以通過 SESSION_ENGINE 配置更改: SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 默認使用數據庫 Cookie 配置 可以通過以下設置自定義 Cookie 行為: SESSION_COOKIE_NAME = 'sessionid' # Cookie 名稱 SESSION_COOKIE_AGE = 1209600 # Cookie 有效期(秒) SESSION_COOKIE_SECURE = True # 僅在 HTTPS 下傳輸 5. 注意事項 request.user 的使用 request.user 是一個 User 對象或 AnonymousUser 對象。 可以通過 request.user.is_authenticated 判斷用戶是否已登入。 登出處理 使用 logout() 函數可以清除 session 數據並登出用戶: from django.contrib.auth import logout logout(request) 6. 範例代碼 以下是一個完整的登入流程範例: from django.contrib.auth import authenticate, login, logout from django.shortcuts import render, redirect def login_view(request): if request.method == 'POST': username = request.POST['username'] password = request.POST['password'] user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('home') # 登入成功後跳轉 else: return render(request, 'login.html', {'error': 'Invalid credentials'}) return render(request, 'login.html') def logout_view(request): logout(request) return redirect('login') # 登出後跳轉 7. 結論 Django 的 login() 函數通過 session 和 Cookie 管理用戶的登入狀態,並提供了方便的 request.user 接口來驗證用戶身份。理解其運作流程有助於開發安全且高效的用戶認證系統。

在 Django 開發中,當我們需要為現有的模型新增欄位時,特別是與其他模型的關聯欄位,可能會遇到一些資料處理上的挑戰。本文將以為 Interview 模型新增 user 欄位為例,說明如何處理資料庫遷移、預設值設定,以及表單資料的處理方式。 為 Interview 模型新增 User 欄位 在 Interview 模型中新增一個與 User 模型的關聯欄位: user = models.ForeignKey(User, on_delete=models.CASCADE) 這樣的設計表示每個 Interview 都必須關聯到一個 User,且當該 User 被刪除時,相關的 Interview 也會被刪除 (on_delete=models.CASCADE)。 makemigrations 時的問題與解決方法 當我們執行 makemigrations 時,如果新增的欄位沒有設定 null=True,Django 會要求我們為現有的資料設定預設值。這是因為資料庫中已經存在的 Interview 資料並沒有 user_id 欄位,而該欄位又不允許為空。 解決方法 建立一個「匿名使用者」帳號 我們可以先註冊一個使用者,並將其名稱設為「匿名使用者」。這樣,所有原本的 Interview 資料都可以關聯到這個使用者。 在 makemigrations 時設定預設值 當執行 makemigrations 時,Django 會提示以下選項: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 選擇 1,並輸入 1 作為預設值,表示將所有現有的 Interview 資料的 user_id 設定為 ID 為 1 的使用者(即「匿名使用者」)。 Select an option: 1 Please enter the default value as valid Python. >>> 1 執行資料庫遷移 接著執行 migrate,更新資料庫結構。這樣,Interview 資料表中會新增 user_id 欄位,且所有現有資料的 user_id 預設為 1。 建立 Interview 資料的方式 在新增 user 欄位後,我們需要在建立 Interview 資料時,確保正確地關聯到當前登入的使用者 (req.user)。以下是兩種常見的處理方式: 作法一:使用 form.save(commit=False) 這是 Django 中處理表單資料的標準方式之一: form = *InterviewForm*(req.POST) interview = form.save(commit=False) # 先將資料準備好,但不存入資料庫 interview.user = req.user # 將當前登入的使用者關聯到 Interview interview.save() # 最後存入資料庫 這種方式的優點是可以在儲存資料前,對資料進行額外的處理,例如設定關聯欄位。 作法二:嘗試字典合併(不可行) 另一種看似可行的方式是將 req.POST 與其他資料合併成一個字典,然後直接傳入表單。但需要注意的是,req.POST 並不是真正的字典,因此這種方法不可行。 Django 的 QueryDict(如 req.POST)的值是列表,即使只有一個值。例如: req.POST = {"name": ["John"], "age": ["30"]} 即使將 req.POST 轉成 python 字典,仍要再將資料轉換型態,因此選用作法一比較適合 字典合併範例 a = {'a': 1} b = {'b': 2} # 字典合併的兩種方式 c = a | b # Python 3.9+ c = {**a, **b} # Python 3.5+ 嘗試將 req.POST 與其他資料合併 form = InterviewForm(req.POST | {"user": req.user}) # 看似可行,但實際上不行 由於 req.POST 是 QueryDict 類型,而不是標準的 Python 字典,因此無法直接使用字典合併的方式處理。 結論 在 Django 中,為模型新增欄位時需要特別注意資料庫遷移的處理,尤其是當欄位不允許為空時。我們可以透過建立「匿名使用者」帳號並設定預設值來解決這個問題。 在處理表單資料時,建議使用 form.save(commit=False) 的方式,這樣可以靈活地對資料進行額外處理,例如設定關聯欄位。 希望本文能幫助你更好地理解 Django 中模型欄位新增與表單資料處理的技巧!

這份筆記記錄了 TimeTracker 工具的設計與實現過程,該工具用於追蹤專案和功能的開發時間,並將數據保存為 JSON 格式,以便後續分析與展示。 一、專案目標 記錄專案開始與結束時間 紀錄每個功能(feature)的開發時間 自動計算持續時間 將數據存儲為 JSON,方便持久化與後續處理 提供命令列介面與 Makefile 支援,簡化操作流程 二、使用技術 datetime:記錄時間戳並計算時間差 json:將追蹤數據序列化為 JSON os:檢查檔案存在性,確保初始化與讀寫流程穩定 sys:處理命令列參數,實現靈活的命令操作 Makefile:簡化常用命令的執行 三、程式架構 my_project/ ├── time_tracking/ │ ├── main.py # 命令列介面入口 │ ├── time_tracker.py # 核心邏輯,負責時間記錄與計算 ├── Makefile # 提供簡化操作的命令 3.1 類別與方法概覽 time_tracker.py 物件導向設計:TimeTracker 類封裝了所有時間追蹤的邏輯。 方法: _load_data():讀取或初始化 JSON 檔案數據 _save_data():將當前數據寫入 JSON 檔案 _current_time():取得當前時間字串 ('%Y-%m-%d %H:%M:%S') _calc_duration(start, end):計算並返回時間差字串 start_project()/end_project():記錄專案整體時間 start_feature(name)/end_feature(name):記錄單一功能的時間 main.py 提供命令列介面,支援以下命令: start_project:開始專案 end_project:結束專案 start_feature <feature_name>:開始功能 end_feature <feature_name>:結束功能 Makefile 提供簡化操作的命令: make start_project:開始專案 make end_project:結束專案 make start_feature feature=<feature_name>:開始功能 make end_feature feature=<feature_name>:結束功能 四、核心實現 4.1 time_tracker.py 的物件導向設計 初始化與數據管理 class TimeTracker: def __init__(self, filename='time_log.json'): self.filename = filename self.data = self._load_data() def _load_data(self): if os.path.exists(self.filename): with open(self.filename, 'r') as f: return json.load(f) return {"project": {}, "features": {}} def _save_data(self): with open(self.filename, 'w') as f: json.dump(self.data, f, indent=4) __init__:初始化 TimeTracker 物件,並讀取或初始化數據。 _load_data():若檔案存在,載入 JSON;否則回傳預設空結構。 _save_data():將當前內存中的 self.data 寫回檔案,保持持久化。 時間處理 def _current_time(self): return datetime.now().strftime('%Y-%m-%d %H:%M:%S') def _calc_duration(self, start, end): start_dt = datetime.strptime(start, '%Y-%m-%d %H:%M:%S') end_dt = datetime.strptime(end, '%Y-%m-%d %H:%M:%S') return str(end_dt - start_dt) _current_time():取得格式化的當前時間。 _calc_duration():計算起訖時間差,並轉為字串。 專案與功能時間追蹤 def start_project(self): self.data['project']['start_time'] = self._current_time() self._save_data() print("Project started.") def end_project(self): self.data['project']['end_time'] = self._current_time() self.data['project']['duration'] = self._calc_duration( self.data['project']['start_time'], self.data['project']['end_time'] ) self._save_data() print("Project ended.") def start_feature(self, name): if name not in self.data['features']: self.data['features'][name] = {} self.data['features'][name]['start_time'] = self._current_time() self._save_data() print(f"Feature '{name}' started.") def end_feature(self, name): feat = self.data['features'].get(name) if feat and 'start_time' in feat: feat['end_time'] = self._current_time() feat['duration'] = self._calc_duration( feat['start_time'], feat['end_time'] ) self._save_data() print(f"Feature '{name}' ended.") else: print(f"Feature '{name}' has not been started.") start_project()/end_project():記錄專案的開始與結束時間,並計算總持續時間。 start_feature(name)/end_feature(name):記錄功能的開始與結束時間,並計算功能持續時間。 4.2 main.py 的命令列介面 import sys from time_tracking.time_tracker import TimeTracker tracker = TimeTracker() def main(): if len(sys.argv) < 2: print("Usage: python main.py [command] [feature_name (optional)]") sys.exit(1) command = sys.argv[1] feature = sys.argv[2] if len(sys.argv) >= 3 else None if command == "start_project": tracker.start_project() elif command == "end_project": tracker.end_project() elif command == "start_feature" and feature: tracker.start_feature(feature) elif command == "end_feature" and feature: tracker.end_feature(feature) else: print("Invalid command or missing feature name.") if __name__ == "__main__": main() 命令列支援: start_project:開始專案。 end_project:結束專案。 start_feature <feature_name>:開始功能。 end_feature <feature_name>:結束功能。 4.3 Makefile 的簡化操作 (用 uv 建置環境) start_project: uv run python -m time_tracking.main start_project end_project: uv run python -m time_tracking.main end_project start_feature: uv run python -m time_tracking.main start_feature $(feature) end_feature: uv run python -m time_tracking.main end_feature $(feature) make start_project:執行 start_project 命令。 make end_project:執行 end_project 命令。 make start_feature feature=<feature_name>:開始功能。 make end_feature feature=<feature_name>:結束功能。 五、改進建議 加入檔案讀寫錯誤處理,提升穩定性。 提供更詳細的狀態檢視功能(如顯示所有功能的持續時間)。 考慮輸出 CSV 或 HTML 報表,便於後續分析。 整合可視化模組(matplotlib, plotly)展示時間分佈圖。 支援更多命令列參數(如自定義輸出檔案名稱)。 六、結語 TimeTracker 是一個輕量且實用的時間追蹤工具,適合個人或小團隊使用。透過物件導向設計與命令列支援,開發者可以輕鬆掌握開發進度並進行效率分析。同時,結合 Makefile 的支援,進一步簡化了操作流程。

在學習 Python 專案結構時,常會遇到一個讓人困惑的問題: 為什麼從某個檔案執行時可以正常 import,換個方式就報錯了? 這背後的關鍵差異,在於你是以「script」還是「module」的方式執行 Python 檔案。 🧱 基本目錄架構 假設我們的專案結構如下: myproject/ ├── main.py └── app/ ├── __init__.py ├── core.py └── utils.py core.py 裡面引用了 utils.py # core.py from app import utils 這看起來是個合理的絕對 import,照理說應該可以用 python app/core.py 執行吧? 實際上會報錯: ModuleNotFoundError: No module named 'app' 🤔 script 與 module 的定義是什麼? script(腳本): 指的是你直接用 python xxx.py 執行的檔案 它會成為 __main__ 模組,也就是程式的進入點 它沒有包在任何 package 裡,也就無法使用相對 import module(模組): 指的是 Python 中可以被 import 的 .py 檔案 可以透過 import 或 from xxx import yyy 引用 用 python -m package.module 方式執行時,模組有正確的 package 上下文 兩者最大的差異在於:script 是執行入口、module 是被載入的單元。當你想讓某個檔案作為 script 直接執行時,它可能會因為沒處於正確的 package 環境而出錯。 ⚠️ 為什麼 python app/core.py 會錯? 這是因為 Python 在執行時,會把你執行的 檔案所在的資料夾 當作第一個 sys.path[0],也就是 Python 搜尋模組的起點。 執行方式 sys.path[0] 設定值 python app/core.py myproject/app python -m app.core myproject(根目錄) 當你在 myproject 根目錄執行: python app/core.py Python 會把 app/core.py 所在的 app/ 資料夾作為起點,這時 from app import utils 就會變成找 app/app/utils.py,結果找不到就報錯。 此外因為 core.py 是 script 模式,沒有包裹在 package 中,也不能使用 from . import utils 相對 import。 ✅ 正確做法:以 module 方式執行 使用 -m 可以指定「從 package 中執行」: cd myproject python -m app.core 這樣 Python 就會把 myproject 加入 sys.path,才能正確找到 app.utils 模組,並允許相對 import。 📌 script vs module 差異整理 執行方式 模式 sys.path[0] 起點 絕對 import 是否成功? 相對 import 是否成功? python app/core.py script 模式 app/ ❌ 失敗 ❌ 無 parent package python -m app.core module 模式 myproject/ ✅ 成功 ✅ 成功 ✅ 最佳實踐 統一從根目錄啟動專案: 使用 main.py 作為 entry point,避免直接執行模組內部的 py 檔案 # main.py from app.core import run if __name__ == "__main__": run() 避免直接執行子模組的 script: python app/core.py 易出錯 請改用 python -m app.core 📚 延伸補充:為什麼 -m 可以正確 import? 使用 -m 時,Python 會把指定 module 所在 package 的上一層目錄加入 sys.path,這讓整個 package 可以正確地相對或絕對 import。 這也符合開發大型專案的需求,讓每個 module 可以互相引用而不報錯。 希望這篇可以幫你釐清 script 與 module 的差異,也知道為什麼在子目錄中就算寫絕對 import 也還是會錯!
0%