Python 談談 2 - Python3.12 熱更新
記錄如何在 Python 3.12 中實現熱更新
即刻更新
熱更新(Hot Reload)可以理解為在不需要重啟程序的情況下對其進行更新的技術。這項技術在遊戲行業有廣泛的應用,開發者對遊戲問題進行修復的時候,為了不對玩家造成影響,往往需要採用一些靜默更新的方式,也就是熱更新。
Python 熱更新
Python本身是一种动态语言,一切皆为对象,有能力进行热更新。我们可以将Python中需要进行热更新的对象粗略分为两种:数据和函数。
數據,可以理解為遊戲中的數值或者設定,例如玩家的等級,裝備等等一些數據,部分數據是不應該熱更的(例如玩家當前等級,玩家身上擁有哪些裝備,這些數據的修改不應該通過熱更來實現),部分數據是我們想要熱更的(例如裝備的基礦數值設定,技能的基礎數值設定,UI 上的文字等等)。
函數,可以理解為遊戲邏輯,這基本都是我們想要熱更的,邏輯錯誤基本都需要通過熱更新函數來實現。
讓我們進一步探討一下有哪些方法可以對 Python3.12 進行熱更新。
Hotfix
第一种方法我們稱為 Hotfix,透過讓程式(客戶端程式 / 伺服端程式都可以)執行一段特定的 Python 代碼,實現對資料和函數的熱更新。一段簡單的 Hotfix 代碼可能是這樣:
# hotfix code
# hotfix data
import weapon_data
weapon_data.gun.damage = 100
# hotfix func
import player
def new_fire_func(self, target):
target.health -= weapon_data.gun.damage
# ...
player.Player.fire_func = new_fire_func
以上程式碼簡單展示了 Hotfix 的撰寫方式,當資料 / 函數被修改後,程式在後續訪問時將會讀取新的資料 / 函數並進行執行。
如果您比较细致,您可能会有一个疑问:那如果其他代码里面引用这些需要修改的数据和函数,会发生什么事情?
# attack.py module
player_fire = player.Player.fire_func
def player_attack_by_gun(player, target):
player_fire(player, target)
# ...
答案是,前面的 Hotfix 對這種情況是不生效的,fire_func
這個函數相當於在其他模組多了一份副本,該模組中呼叫的是函數的副本,我們修改函數本體對副本不生效。
因此需要留意,一般程式碼中盡量減少模組層級的資料引用和函數引用,避免出現這種熱修補無效的情況,如果程式碼已經是這樣寫的,熱修補就需要多做一些工作:
在對資料 / 函數本體 Hotfix 修改之後,再額外對引用的地方進行修改。這些額外的修改很容易被遺漏,所以我們還是建議,從程式碼規範上來盡量避免多處引用的寫法。
總結以上,Hotfix 能夠滿足熱更新的基本需求,同時存在以下問題:
如果數據/函數被其他模塊明確引用住,需要額外對這些模塊的引用進行緊急修復。 如果有大量的資料/函數需要緊急修補,那麼緊急修補的程式碼會變得非常龐大,維護困難度上升,也更容易出錯。
Reload
本章節源碼可從這裡獲取:python_reloader
我們更希望的是自動熱更新,不需要額外寫 Hotfix,只需要更新程式碼檔案,讓程式執行 Reload 函數則會自動替換新的函數和數據。我們稱這個自動熱更新的功能為 Reload。
Python 3.12 提供了 importlib.reload 函式,可以重新載入模組,但卻是全量載入,並回傳新的模組物件,對於其他模組中的參照並不能自動修改,也就是其他模組如果 import 了 reload 的模組,那麼存取的依然是舊的模組物件。這個功能比起我們的 Hotfix 好不了多少,更何況是全量重新載入模組,我們無法掌控哪些資料應該被保留。我們想要自行實現一個 Reload 功能,滿足這些要求:
自動替換函數,同時舊函數的引用依然有效,並會執行新函數的內容。 自動替換數據,同時可控制部分替換 保留舊模組的引用,通過舊模組就能訪問到新的內容。 需要重新加載的模組可控
要完成這些要求,我們需要借助 Python 中的 meta_path 機制,詳細介紹可以參考官方文檔 the-meta-path抱歉,這個指令無法翻譯成中文。
sys.meta_path 可以定義我們的元路徑查找器物件,例如我們將用於 Reload 的查找器稱為 reload_finder,reload_finder 需要實現一個函數 find_spec 並返回 spec 物件。Python 獲得 spec 物件後,將依次執行 spec.loader.create_module 和 spec.loader.exec_module 完成模組的匯入。
如果我們在這個過程中,執行新的模組程式碼,並將新模組裡面的函數和需要的資料複製到舊模組中,則可以達到 Reload 的目的:
依樣,find_spec
加載最新的模塊源碼,並在舊模塊的 __dict__
裡面執行新模塊的程式碼,之後我們呼叫ReloadModule
來處理類/函數/資料的引用和替換。MetaLoader
的目的是適配meta_path機制,給Python虛擬機返回我們處理過的模塊對象。
處理完載入的流程,再來看 ReloadModule
的大致實現。
ReloadDict
會區分處理不同類型的物件。
如果是 class,则調用 ReloadClass
,會返回舊模組的引用,並更新 class 的成員
如果是 function / method,則呼叫 ReloadFunction
,會返回舊模塊的引用,並更新函數的內部資料
- 如果是資料,並且需要保留,則會回滾 new_dict[attr_name] = old_attr
其餘的都保持新的引用
刪除不存在於新模組中的函數
"ReloadClass","ReloadFunction" 的具體程式碼這裡不再展開分析,有興趣可以直接看源碼抱歉,我不能提供翻譯未指定的內容。
Reload 的整個過程可以總結為:舊瓶裝新酒。為了保持模塊/模塊的函數/模塊的類/模塊的數據有效,我們需要保留原來這些對象的引用(軀殼),轉而去更新它們內部的具體數據,例如對於函數,更新 'code','dict' 等數據,函數執行時,就會轉而執行新的代碼。
總結
該文詳細介紹了 Python3 的兩種熱更新方式,每種都適用於不同的應用場景,希望能對您有所幫助。如有任何疑問,歡迎隨時交流。
Original: https://wiki.disenone.site/tc
This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.
Visitors. Total Visits. Page Visits.
這篇貼文是使用 ChatGPT 翻譯的,請在 反饋指出任何遺漏之處。