Python進階(一)

博客鏈接: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日于杭州家中

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.元類 1.1.1類也是對象 在大多數(shù)編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這...
    TENG書閱讀 1,418評論 0 3
  • http://python.jobbole.com/85231/ 關(guān)于專業(yè)技能寫完項目接著寫寫一名3年工作經(jīng)驗的J...
    燕京博士閱讀 7,794評論 1 118
  • 雖然是自己轉(zhuǎn)載的但是是真的好的一篇圖文并茂的對垃圾回收機制的講解!!! 先來個概述,第二部分的畫述才是厲害的。 G...
    東皇Amrzs閱讀 119,292評論 13 175
  • 如果有一天我離開了,誰還會記得我? 同事,很快會連我的名字都忘記。 泛泛之交,甚至都不知道我已離去。 朋友,很難說...
    芳草有情閱讀 291評論 1 2
  • 一直不知道什么是家,最近一個人待的久了才明白原來家很簡單。家不是高樓大廈豪華裝修,也不是燈紅酒綠美女如群。家是...
    陳澤蕭閱讀 734評論 1 4

友情鏈接更多精彩內(nèi)容