有時候我們可能需要只修改一部分代碼而且要求修改立即生效,或者為了高可用性不允許停止服務程序,這時我們就需要熱補丁

在debian,red hat等系統(或者vista之後的windows)的軟件更新時,通常使用替換符號鏈接來達到高可用性。

對Python來說,解釋器預先處理了腳本生成位元組碼,並讀入內存;所以之後硬盤上的文件發生了什麼變化,就只能想辦法命令解釋器重新讀入新的腳本。實現這個功能的內建命令是reload

以下例子的語法以Python2.x為準,但是除了語法外基本思想與Python3.x通用。了解語法差異,請查看本文的Python3

使用reload

引自python-docs

When reload(module) is executed:

  • Python modules』 code is recompiled and the module-level code reexecuted, defining a new set of objects which are bound to names in the module』s dictionary. The initfunction of extension modules is not called a second time.
  • As with all other objects in Python the old objects are only reclaimed after their reference counts drop to zero.
  • The names in the module namespace are updated to point to any new or changed objects.
  • Other references to the old objects (such as names external to the module) are not rebound to refer to the new objects and must be updated in each namespace where they occur if that is desired.

也就是說reload更新了命名空間中對模塊名稱的指向。我們來試一下:

在解釋器中輸入

修改磁盤上的文件成

在解釋器中輸入

這的確是我們想要的結果。

有什麼問題呢

繼承關係

在上面這個簡單的例子里我們絲毫沒有發現問題,代碼運行得很好。但是考慮這種情況:

假設保存在test.py中。然後假設另一模塊run.py調用了test:

這樣當我們運行run.py時,會依次輸出:

sub init
True

sub init
False

可見reload之後,isinstance得到了不是我們想要的結果。通過id打印內存可以解釋這個問題,我們在test.py的isinstance前加一行

用同樣的方法,在run.py中打印sub_class,得到輸出:

test.sub_class at 0x029385A8
sub init
super_class: 0x028DEFB8
True

test.sub_class at 0x029385A8
sub init
super_class: 0x0291B340
False

由此可見,reload(sub_class)之後,test下的所有類被重新加載,內存位置發生了變化。假設舊的模塊還沒被GC掉,這時內存中同時存在了新老兩個版本的test模塊,以及模塊中的所有變量。為了方便起見,我們講老test模塊中類統一稱作老xxx,新test模塊中的類成為新xxx。

reload(test)之後,run.py模塊的命名空間中,sub_class仍然指向舊sub_class(兩次的id值一致),而從sub_class的構造函數中引用的super_class的內存位置卻發生了變化。新sub_class是新super_class的子類,舊sub_class是舊super_class的子類,但舊super_class不能是新super_class的子類。

為了方便解釋,我們可以畫一個內存的引用圖來解釋這個變化:

(還沒畫)

要解決這個問題,可以在reload模塊後再執行一次from xxx import yyy,或者直接用模塊名.類名的方式引用,這樣始終使用同一版本的super_class和sub_class,也就不會出現問題:

灰度發佈

和上面的問題相同,我們也可以通過使用之前提到的兩種方法來保持使用的父類和子類始終是配套的。

但是有時的情況是,我們希望同時存在保持新舊兩套版本,比如,灰度發佈時,系統中可能同時存在兩套代碼。這時能不能在老sub_class中繼續引用到老super_class呢。

  1. 檢查是否是子類的方法有一點黑科技,看一下父類的類名在不在__bases__里就可以了。因為一個模塊下同樣的類名只會出現一次,所以這種方法是可靠的。
  2. 調用父類方法,我們可以使用super()函數,前提是類是新式類(new-style classes),只要最大輩分的類繼承object就可以了。將test.py修改如下:

如果最大輩分的類無法修改,可以在子類中繼承object,即

Python3

在Python3.x中,test.py的super函數不需要傳遞參數:

super().__init__()

reload移動到了imp中,使用reload前需要先:

from imp import reload

隨便扯兩句

這篇文章其實最早寫於2014年5月,寫了一段之後不想寫了2333,那個時候MAClient的網頁版剛完成了一個熱更新的commit,意味着可以不停止老用戶的實例而更新新版本的代碼進去。前兩天有基友跟我說起這個腳本,跑了一下竟然還能跑,看了一下裏面快滿出來的紅茶綠茶,還是有很多回憶(我還有幾個沒有公開的刷道具bug呢哈哈哈哈哈):

無標題

MAClient是我到現在為止寫過的最大的一個項目,一共大概有1萬行左右。很多新的構想都是從這裡開始來的,比如這個熱更新,以及插件的思想。沒錯我就是在裝逼

以後寫個關於插件的文章吧,感覺蠻有意思的20160109213156