Python代碼模塊熱更新機制實現(reload)

 對一個遊戲來說,無論是client或server都非常需要一套代碼熱更新的機制。它能大大提高開發效率,又能超乎玩家期望地在運營期在線修正bug和增添功能。可謂必備機制。

熱更新機制的目標是:
  (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。這個限制對派生類一樣存在。


  會發現我們其實更希望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));
     所以,要實現這點,只需要遵守一個原則:保持對象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處理更徹底。


reload的封裝使用:

import sys  
import os  
class Reloader:  
    SUFFIX = '.pyc'  
  
    def __init__(self):  
        self.mtimes = {}  
  
    def __call__(self):  
        import pdb  
        pdb.set_trace()  
        for mod in sys.modules.values():  
            self.check(mod)  
  
    def check(self, mod):  
        if not (mod and hasattr(mod, '__file__') and mod.__file__):  
            return   
        try:  
            mtime = os.stat(mod.__file__).st_mtime  
        except (OSError, IOError):  
            return  
        if mod.__file__.endswith(self.__class__.SUFFIX) and os.path.exist(mod.__file__[:-1]):  
            mtime = max(os.stat(mod.__file__[:-1].st_mtime), mtime)  
        if mod not in self.mtimes:  
            self.mtimes[mod] = mtime  
        elif self.mtimes[mod] < mtime:  
            try:  
                reload(mod)  
                self.mtimes[mod] = mtime  
            except ImportError:  
                pass  
  
reloader = Reloader()  
reloader()  

 第一個知識點:
self.__class__.SUFFIX,首先python一切皆對象,類實際上在python的世界裏面也是一個對象,__class__至少在下面這種情況中是有用的:當一個類中的某個成員變量是所有該類的對象的公共變量時.
很像c++中的static變量。
第二個知識點:
__import__與reload的區別:
(1):多次重複使用import語句時,不會重新加載被指定的模塊,只是把對該模塊的內存地址給引用到本地變量環境。
(2)對已經加載的模塊進行重新加載,一般用於原模塊有變化等特殊情況,reload前該模塊必須已經import過。reload會重新加載已加載的模塊,但原來已經使用的實例還是會使用舊的模塊,而新生產的實例會使用新的模塊;reload後還是用原來的內存地址;不能支持from。。import。。格式的模塊進行重新加載。 
(3)通常在動態加載時可以使用到這個函數,比如你希望加載某個文件夾下的所用模塊,但是其下的模塊名稱又會經常變化時,就可以使用這個函數動態加載所有模塊了,最常見的場景就是插件功能的支持。比如__import__可以這樣__import__('os',globals(),locals(),['path','pip']) ,reload就不行了。
具體可以參考看看這篇文章http://www.cnblogs.com/MaggieXiang/archive/2013/06/05/3118156.html
第三個知識點:
__call__的用法,可調用對象,這個確實是第一次接觸到,大概的用法就是我們只需要去重載這個方法,那麼我們就可以像調用方法的形式去調用對象,那麼它就會去回調這個__call__方法,具體的好處可以參看這個博客http://hi.baidu.com/feng2211/item/d55d0415602bfcfcdceeca45
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章