標籤:

Python進階課程筆記(一)

知乎專欄申請通過,把之前在簡書上關於Python整理的技術文章移動過來。

這周聽了三節Python進階課程,有十幾年的老程序給你講課傳授一門語言的進階知識,也許這是在大公司才能享受到的福利。雖然接觸使用Python也有三四年時間了,但是從課程中還是學習到不少東西,掌握了新技巧的用法,明白了老知識背後的原因。

下載了課件,做了筆記,但我還是希望用講述的方式把它們表現出來,為未來的自己,也給需要的讀者。整體以大雄的課程為藍本,結合我在開發中的一些自己的體會和想法。

1. 寫操作對於命名空間的影響

首先來看這樣一段代碼:

import mathnndef foo(processed):n value = math.pinn # The other programmer add logic here.n if processed:n import mathn value = math.sin(value)nn print valuennfoo(True)n

思考:你覺得這段代碼有沒有什麼問題,它的運行結果是什麼?

首先,我個人不喜歡在代碼中進行import math的操作的方式,通常會建議把這一操作放置到文件頭部,這主要處於性能的考慮——雖然已經import過的模塊不會重複執行載入過程,但畢竟有一次從sys.modules中查詢的過程。這種操作在tick等高頻執行的邏輯中尤其要去避免。

但這並不是這段代碼的問題所在的重點,當你嘗試執行這段代碼的時候,會輸出如下的錯誤:

Traceback (most recent call last):n File "C:UsersDavid-PCDesktopAdvanced Course on Python 2016t019.py", line 13, in <module>n foo(True)n File "C:UsersDavid-PCDesktopAdvanced Course on Python 2016t019.py", line 4, in foon value = math.pinUnboundLocalError: local variable math referenced before assignmentn

在賦值之前被引用了,這似乎是在文件頭部進行import的鍋。這個例子稍微有點複雜,我們嘗試寫一段有點近似但是更簡單的例子,在之前編碼過程中我就遇到過類似的情況:

value = 0ndef foo():n if value > 0:n value = 1n print valuenfoo()n

同樣會提示value在被賦值之前被使用了,讓這段代碼正常運作很簡單,只需要把global value放在foo函數定義的第一行就可以了。

思考: 為什麼在foo函數內部,無法訪問其外部的value變數?

如果你把value = 1這一行代碼注釋掉,這段代碼就可以正常運行,看上去對於value的賦值操作導致了我們無法正常訪問一個外部的變數,無論這個賦值操作在訪問操作之前還是之後。

Write operation will shield the locating outside the current name space, which is determined at compile time.

簡單來說,命名空間內部如果有對變數的寫操作,這個變數在這個命名空間中就會被認為是local的,你的代碼就不能在賦值之前使用它,而且檢查過程是在編譯的時候。使用global關鍵字可以改變這一行為。

那我們回到第一段代碼,為什麼imort的一個模塊也無法正常被使用呢?

如果理解import的過程,答案就很簡單了——import其實就是一個賦值的過程

總結:之前我自認為Python的命名空間很容易理解,對於全局變數或者說upvalue的訪問卻通常不去注意,有時候覺得不需要寫global來標識也可以訪問得到,有時候又會遇到語法錯誤的提示,其實一直沒有理解清楚是什麼規則導致這樣的結果。

寫操作對於命名空間的影響解答了這一問題,讓我看到自己之前「面對出錯提示編程」的愚蠢和懶惰。。。

2. 循環引用

Python的垃圾回收(GC)結合了引用計數(Reference Count)、對象池(Object Pool)、標記清除(Mark and Sweep)、分代回收(Generational Collecting)這幾種技術,具體的GC實現放在後面來說,我們先看代碼中存在循環引用的情況。

遊戲開發中設計出循環引用非常地簡單,比如遊戲中常用的實體(Entity)結構:

class EntityManager(object):n def __init__():n self.__entities = {}nn def add_entity(eid):n #Some process code.n self.__entities[eid] = Entity(id, self)nn def get_entity(eid):n return self.__entities.get(eid, None)nnclass Entity(object):n def __init__(eid, mgr):n self.eid = _idn self.mgr = mgrnn def attact(skill_id, target_id):n target = self.mgr.get_entity(target_id)n #attack the targetn #...n

很明顯,這裡EntityManager中的__entities屬性引用了它所控制的所有對象,而對於一個遊戲實體,有時候需要能夠獲取別的實體對象,那麼最簡單的方法就是把EntityManager的自己傳遞給創建出來的實體,讓其保留一個引用,這樣在執行攻擊這樣的函數的時候,就可以很方便地獲取到想要拿到的數據。

EntityManager中的__entities屬性引用了Entity對象,Entity對象身上的mgr屬性又引用了EntityManager對象,這就存在循環引用。

有的人也許會說,有循環引用了,so what? 首先我可以從邏輯上保證釋放的時候都會把環解開,這樣就可以正常釋放內存了。再者,本身Python自己就提供了垃圾回收的方式,它可以幫我清理。

對於這種想法,作為一個遊戲開發者,我表示——呵呵

我們看一個在遊戲開發中常見的循環引用的例子,有些情況下寫了循環引用而不自知(實例代碼直接使用大雄課程中的)。

class Animation(object):n def __init__(self, callback):n self._callback = callbacknnclass Entity(object):n def __init__(self):n self._animation = Animation(self._complete)nn def _complete(self):n passnne = Entity()nprint e._animation._callback.im_self is en

最終print輸出的結果是True,也解釋了這段邏輯中的循環引用所在。

對於多人協作來實現的大型項目來說,邏輯上保證代碼中沒有環存在是幾乎不可能的事情,況且即使你代碼邏輯上可以正確釋放,偶發的traceback就可能讓你接環的邏輯沒有被執行到,從而導致了循環引用對象的無法立即釋放。

Python的循環引用處理,如果一個對象的引用計數為0的時候,該對象會立即被釋放掉。

然後Python的GC是很耗的一個過程,會造成CPU瞬間的峰值等問題,網易有項目就完全自己實現了一套分片多線程的GC機制來替換掉Python原生的GC。

大量循環引用的存在會導致更慢更加頻繁的GC,也會導致內存的波動。

解決方法:對於EntityManager的例子,使用weakref來解決;對於callback的例子,盡量避免使用對象的方法來作為一個回調。

總結:對於簡單的系統來說,不需要關心循環引用的問題,交給Python的GC就夠了,但是需要長時間運行,對於CPU波動敏感的系統,需要關注循環引用的影響,盡量去規避。

題外話:在我們現在的項目中,EntityManager的例子使用了單例模式來解除循環引用,這是一種常用的方法,但是單例模式也不是「銀彈」。這種設計模式在限制對象實例化的同時,也提供了全局訪問的介面,意味著這個單例對象變成了一個全局對象,於是代碼中充滿了不考慮耦合性的濫用。在客戶端代碼中,這些使用全局單例的邏輯沒有問題,因為客戶端只需要一個EntityManager就可以管理所有的遊戲實體,也不會存在其他的並行環境,而當我們需要進行服務端開發的時候,同一份代碼拿到服務端就變成了災難——對於服務端來說,可能會存在很多EntityManager管理不同情境下的遊戲實體,單例的模式不再可用,之前任意訪問EntityManager的地方都需要經過迭代和整理才可以正常執行。

2016年6月30日於杭州家中


推薦閱讀:

TAG:Python |