博客鏈接:http://inarrater.com/2016/06/30/pythonadvance1/
這周聽了三節(jié)Python進階課程,有十幾年的老程序給你講課傳授一門語言的進階知識,也許這是在大公司才能享受到的福利。雖然接觸使用Python也有三四年時間了,但是從課程中還是學(xué)習(xí)到不少東西,掌握了新技巧的用法,明白了老知識背后的原因。
下載了課件,做了筆記,但我還是希望用講述的方式把它們表現(xiàn)出來,為未來的自己,也給需要的讀者。整體以大雄的課程為藍(lán)本,結(jié)合我在開發(fā)中的一些自己的體會和想法。
1. 寫操作對于命名空間的影響
首先來看這樣一段代碼:
import math
def foo(processed):
value = math.pi
# The other programmer add logic here.
if processed:
import math
value = math.sin(value)
print value
foo(True)
思考:你覺得這段代碼有沒有什么問題,它的運行結(jié)果是什么?
首先,我個人不喜歡在代碼中進行import math的操作的方式,通常會建議把這一操作放置到文件頭部,這主要處于性能的考慮——雖然已經(jīng)import過的模塊不會重復(fù)執(zhí)行加載過程,但畢竟有一次從sys.modules中查詢的過程。這種操作在tick等高頻執(zhí)行的邏輯中尤其要去避免。
但這并不是這段代碼的問題所在的重點,當(dāng)你嘗試執(zhí)行這段代碼的時候,會輸出如下的錯誤:
Traceback (most recent call last):
File "C:\Users\David-PC\Desktop\Advanced Course on Python 2016\t019.py", line 13, in <module>
foo(True)
File "C:\Users\David-PC\Desktop\Advanced Course on Python 2016\t019.py", line 4, in foo
value = math.pi
UnboundLocalError: local variable 'math' referenced before assignment
在賦值之前被引用了,這似乎是在文件頭部進行import的鍋。這個例子稍微有點復(fù)雜,我們嘗試寫一段有點近似但是更簡單的例子,在之前編碼過程中我就遇到過類似的情況:
value = 0
def foo():
if value > 0:
value = 1
print value
foo()
同樣會提示value在被賦值之前被使用了,讓這段代碼正常運作很簡單,只需要把global value放在foo函數(shù)定義的第一行就可以了。
思考: 為什么在foo函數(shù)內(nèi)部,無法訪問其外部的value變量?
如果你把value = 1這一行代碼注釋掉,這段代碼就可以正常運行,看上去對于value的賦值操作導(dǎo)致了我們無法正常訪問一個外部的變量,無論這個賦值操作在訪問操作之前還是之后。
Write operation will shield the locating outside the current name space, which is determined at compile time.
簡單來說,命名空間內(nèi)部如果有對變量的寫操作,這個變量在這個命名空間中就會被認(rèn)為是local的,你的代碼就不能在賦值之前使用它,而且檢查過程是在編譯的時候。使用global關(guān)鍵字可以改變這一行為。
那我們回到第一段代碼,為什么imort的一個模塊也無法正常被使用呢?
如果理解import的過程,答案就很簡單了——import其實就是一個賦值的過程。
總結(jié):之前我自認(rèn)為Python的命名空間很容易理解,對于全局變量或者說upvalue的訪問卻通常不去注意,有時候覺得不需要寫global來標(biāo)識也可以訪問得到,有時候又會遇到語法錯誤的提示,其實一直沒有理解清楚是什么規(guī)則導(dǎo)致這樣的結(jié)果。
寫操作對于命名空間的影響解答了這一問題,讓我看到自己之前“面對出錯提示編程”的愚蠢和懶惰。。。
2. 循環(huán)引用
Python的垃圾回收(GC)結(jié)合了引用計數(shù)(Reference Count)、對象池(Object Pool)、標(biāo)記清除(Mark and Sweep)、分代回收(Generational Collecting)這幾種技術(shù),具體的GC實現(xiàn)放在后面來說,我們先看代碼中存在循環(huán)引用的情況。
游戲開發(fā)中設(shè)計出循環(huán)引用非常地簡單,比如游戲中常用的實體(Entity)結(jié)構(gòu):
class EntityManager(object):
def __init__():
self.__entities = {}
def add_entity(eid):
#Some process code.
self.__entities[eid] = Entity(id, self)
def get_entity(eid):
return self.__entities.get(eid, None)
class Entity(object):
def __init__(eid, mgr):
self.eid = _id
self.mgr = mgr
def attact(skill_id, target_id):
target = self.mgr.get_entity(target_id)
#attack the target
#...
很明顯,這里EntityManager中的__entities屬性引用了它所控制的所有對象,而對于一個游戲?qū)嶓w,有時候需要能夠獲取別的實體對象,那么最簡單的方法就是把EntityManager的自己傳遞給創(chuàng)建出來的實體,讓其保留一個引用,這樣在執(zhí)行攻擊這樣的函數(shù)的時候,就可以很方便地獲取到想要拿到的數(shù)據(jù)。
EntityManager中的__entities屬性引用了Entity對象,Entity對象身上的mgr屬性又引用了EntityManager對象,這就存在循環(huán)引用。
有的人也許會說,有循環(huán)引用了,so what? 首先我可以從邏輯上保證釋放的時候都會把環(huán)解開,這樣就可以正常釋放內(nèi)存了。再者,本身Python自己就提供了垃圾回收的方式,它可以幫我清理。
對于這種想法,作為一個游戲開發(fā)者,我表示——呵呵
我們看一個在游戲開發(fā)中常見的循環(huán)引用的例子,有些情況下寫了循環(huán)引用而不自知(實例代碼直接使用大雄課程中的)。
class Animation(object):
def __init__(self, callback):
self._callback = callback
class Entity(object):
def __init__(self):
self._animation = Animation(self._complete)
def _complete(self):
pass
e = Entity()
print e._animation._callback.im_self is e
最終print輸出的結(jié)果是True,也解釋了這段邏輯中的循環(huán)引用所在。
對于多人協(xié)作來實現(xiàn)的大型項目來說,邏輯上保證代碼中沒有環(huán)存在是幾乎不可能的事情,況且即使你代碼邏輯上可以正確釋放,偶發(fā)的traceback就可能讓你接環(huán)的邏輯沒有被執(zhí)行到,從而導(dǎo)致了循環(huán)引用對象的無法立即釋放。
Python的循環(huán)引用處理,如果一個對象的引用計數(shù)為0的時候,該對象會立即被釋放掉。
然后Python的GC是很耗的一個過程,會造成CPU瞬間的峰值等問題,網(wǎng)易有項目就完全自己實現(xiàn)了一套分片多線程的GC機制來替換掉Python原生的GC。
大量循環(huán)引用的存在會導(dǎo)致更慢更加頻繁的GC,也會導(dǎo)致內(nèi)存的波動。
解決方法:對于
EntityManager的例子,使用weakref來解決;對于callback的例子,盡量避免使用對象的方法來作為一個回調(diào)。
總結(jié):對于簡單的系統(tǒng)來說,不需要關(guān)心循環(huán)引用的問題,交給Python的GC就夠了,但是需要長時間運行,對于CPU波動敏感的系統(tǒng),需要關(guān)注循環(huán)引用的影響,盡量去規(guī)避。
題外話:在我們現(xiàn)在的項目中,EntityManager的例子使用了單例模式來解除循環(huán)引用,這是一種常用的方法,但是單例模式也不是“銀彈”。這種設(shè)計模式在限制對象實例化的同時,也提供了全局訪問的接口,意味著這個單例對象變成了一個全局對象,于是代碼中充滿了不考慮耦合性的濫用。在客戶端代碼中,這些使用全局單例的邏輯沒有問題,因為客戶端只需要一個EntityManager就可以管理所有的游戲?qū)嶓w,也不會存在其他的并行環(huán)境,而當(dāng)我們需要進行服務(wù)端開發(fā)的時候,同一份代碼拿到服務(wù)端就變成了災(zāi)難——對于服務(wù)端來說,可能會存在很多EntityManager管理不同情境下的游戲?qū)嶓w,單例的模式不再可用,之前任意訪問EntityManager的地方都需要經(jīng)過迭代和整理才可以正常執(zhí)行。
PPS:剛剛開始使用MarkDown,一些語法還不熟悉,但是用它寫起這種包含代碼的文章來說還是非常舒服的,這篇開頭讓我體會到了這一點。所以說,只有程序員這種geek生物才會喜歡Hexo等基于MarkDown需要generate成靜態(tài)網(wǎng)頁的博客。。。
2016年6月30日于杭州家中