天天看點

Python代碼子產品熱更新機制實作(reload)

---------------------------------------------------------------------

  對一個遊戲來說,無論是client或server都非常需要一套代碼熱更新的機制。

它能大大提高開發效率,又能超乎玩家期望地在營運期線上修正bug和增添功能。

可謂必備機制。

---------------------------------------------------------------------

  在實作一個Python版本的熱更新系統時,我走了不少彎路,鑽了很多牛角尖,

完成了一個我看來有很多細節不夠完善不夠強大的版本,并在想做到'更完美'。

  直到和同僚DX,KHF讨論後,才發現最重要的是明确熱更新機制的目标是什麼。

  往往我們善于将小事一件件做好,但卻忘記問問自己為什麼前行。

  我要作的熱更新機制的目标是:

  (1)更新代碼定義

  (2)不更新資料對象

  (3)不要依賴熱更新機制解決所有問題。過于複雜的改動,重新開機程序

  具體到Python這個語言而言,目标便是:

  (1)更新類/函數及衍生對象:class/function/method/classmethod/staticmethod

  (2)不更新除了(1)中的其他類型對象

  (3)不要依賴熱更新機制解決所有問題。過于複雜的改動,重新開機程序

  第(3)點将我解救出來了:不要把所有責任壓在熱更新機制上。

  本文所指子產品隻限于.py/.pyc/.pyo...(即非dll/so/bulitin)為載體的子產品。

---------------------------------------------------------------------

  Python的__builtins__中有一個衆所周知的reload,但它在大項目中的可用性

幾乎為零也是衆所周知的。它辜負了Python Documentation中對它的評價:

  "This is useful if you have edited the module source file using an 

  external editor and want to try out the new version without leaving 

  the Python interpreter"

---------------------------------------------------------------------  

  這裡簡單翻譯一下Python内建的reload的說明:

  當reload(M)被執行後:

  * M子產品将被重新解釋位元組碼。 并再執行子產品級定義的執行語句(譯注:由此應

    認識到在子產品級就編寫函數調用和類對象生成是多麼壞的習慣呀)。

    并在M子產品内定義一個新的 命名->新對象 的命名空間映射。

  * M子產品reload前的所有舊對象,直到它們的引用數量降到0,才可能被gc回收。

  * M子產品的命名空間中的命名全部指向了新的對象。

  * 其他子產品中對M子產品reload前的舊對象的引用,仍然維持舊對象的引用;

    如果你希望其他子產品對M子產品的相關對象引用能同時更新為M中的新對象,

    那需要你自己動手。

  一些reload函數的注意事項:

  * 如果舊的子產品M命名空間中的某個命名x在修改後的子產品M中不存在,

    那reload(M)後,M.x仍然有效,并繼續引用着reload(M)前的那個對象。

    (譯注:由于reload存在這個設定,是以下面要實作的reloadx将實作不了

     一個功能:即使修改子產品M來删除命名,reloadx也不能删除原子產品命名空間

     内的命名!)

  * 由于存在上面一個設定,一個防止資料對象被reload重置的編碼方案是:

    try:

        users

    except NameError:

        users = {"AKara", "Sheldon Cooper"}

  * 如果子產品B使用 from M import ... 的方式從子產品M中導入對象引用,

    那麼reload(M)不會令B中的已導入對象産生任何影響;

    如果你需要實作這種影響,那需要自己動手在執行一次from .. import;

    又或者修改代碼,使用 M.name 的方式來引用A中的對象。

  * 如果一個子產品已經産生了它的某個class的instance,

    那重定義這個class并reload這個子產品,并不能影響已經存在的instance

    的class————這個instance還在用着reload前的class。這個限制對派生類

    一樣存在。

---------------------------------------------------------------------  

  看完Python内建reload說明;

  會發現我們其實更希望reload應該至少長成這樣子:

  [1] reload(M)後,所有reload前生成的M中的類的instance(無論它在哪裡),

    自動引用新的類實作。

  [2] reload(M)後,所有對M中的function對象的引用(無論以什麼方式引用),

    自動更新到新版本函數定義。

  [3] 不需要 try .. except NameError 的編碼方式,便能令reload不重置資料對象。

    即所有cls inst,dict, list, set, frozenset, tuple, string, None, Boolean...

    對象複用舊對象。

  有了功能需求定義,再聯系上面的[熱更新機制的目标],不妨實作一個reloadx。

  實作的核心思路有兩種:

  (思路1)

     Python中,一切皆為對象。(有人歡喜有人愁呀;Python的慢是有理由的)

     顯然,function/method/staticmethod/classmethod/class 均為對象。

     而變量名和對象之間的關系其實隻是一種命名空間和對象空間中的引用映射

     (或許這事實困擾不少初學者:"Python函數傳參到底是傳值還是傳位址?"),

     而對象空間中的每個對象是唯一的,有唯一的address(即id(obj));

     是以,要實作第[1][2]點,隻需要遵守一個原則:

      保持對象address不變,也即是保證reloadx前後的對象是同一個對象!

     乍聽起來很沖突,但是大體上是可以的:

     method / staticmethod / classmethod / function這四種對象類型其實

     都可以歸結到function object的更新上(因為method/staticmethod/classmethod

     本質上都是對function的一個wrapper對象,都有途徑獲得被wrap的function)。

     而function object的功能其實本質上是一個函數塊,

     它主要由func_code, func_defaults, func_doc三個成員組成,

     那我們用reload後的function對象相應内容替換到舊的function對象中即可。

     而class則稍微特殊一些,它是由method / staticmethod / classmethod, 

     以及BASES關系(+MRO),資料成員等共同組成的一個對象體。

     但由于Python中對BASES tuple在運作時的替換有deallocator相等的限制,使得

     從Python腳本層次對派生關系重新定義不可行(但是增加基類是可以的:

     ClassA.__bases__ += (ClassB, ) ,所謂的Mix-in),真遺憾。

     不過函數和方法的更新是沒問題的,替換方法和函數已經滿足大部分的需求了。

     這個思路有幾個優點:

     - 無論這些function/class以什麼方式引用,

       隻要不深入直接引用到func_code/func_default對象,均可動态更新到

     - 隻需要更新一個對象,速度非常快

     當然也有缺點:

     - 不能動态更新class的派生關系相關的資訊,非常遺憾

  (思路2)

     子產品M被熱更新後,找出所有對M中的class/function...有引用的對象,

     逐個執行新對象替換舊對象的操作。比如obj.__class__ = class_after_reload。

     這個思路的優點是:

     - 實作相對簡潔

     - 支援class對象的全更新

     缺點是:

     - 對于将function/classobj.method跨子產品不可變容器(tuple, frozenset...)引用的更新不了

     - 如果引用對象衆多,比(思路1)處理起來慢許多。

--------------------------------------------------------------------- 

  實作之前搭建一個簡單的可持續測試環境,再實作reloadx,然後針對一些

複雜用例進行反複測試(這是個漫長的過程)。

  最終我實作了一個(思路1)的機制。機制伴随着幾個約定的子產品級函數調用,

友善完成一些reload前後和子產品初始化的資料定制。實作了reloadx後,對

編寫Python的良好子產品的了解又進了一步。最好項目一開始便要實行系列規範。

  後續可能還有一些改進措施可以做:

  (1) 是否可以通過一些命名約定來實作子產品級的 dict / list / set 等資料更新?

  (2) 如果(1)可以實作,考慮實作 tuple frozenset 之類的固态容器更新?

  (3) 監測兩次update之間是否存在對象洩漏,防止reloadx多次後記憶體增大。

  (4) 如果想偷懶,還可以開一個Python thread定時檢查所有py的修改時間,

      自動reloadx。

  (5) 實作(思路2)的版本對class處理更徹底。

---------------------------------------------------------------------