動態語言的靈活性是把雙刃劍 -- 以Python語言爲例

本文有些零碎,總題來講,包括兩個問題:html

(1)可變對象(最多見的是list dict)被意外修改的問題,python

(2)對參數(parameter)的檢查問題。編程

這兩個問題,本質都是由於動態語言(動態類型語言)的特性形成了,動態語言的好處就不細說了,本文是要討論由於動態--這種靈活性帶來的一些問題。設計模式

什麼是動態語言(Dynamic Programming language)呢,是相對於靜態語言而言,將不少靜態語言編譯(compilation)時期所作的事情推遲到運行時,在運行時修改代碼的行爲,好比添加新的對象和函數,修改既有代碼的功能,改變類型。python3.x

絕大多數動態語言都是動態類型(Dynamic Typed),所謂動態類型,是在運行時肯定數據類型,變量使用以前不須要類型聲明,一般變量的類型是被賦值的那個值的類型。Python就是屬於典型的動態語言。安全

動態語言的魅力在於讓開發人員更好的關注須要解決的問題自己,而不是冗雜的語言規範,也不用幹啥都得寫個類。運行時改變代碼的行爲也是很是有用,好比python的熱更新,能夠作到不關服務器就替換代碼的邏輯,而靜態語言如C++就很難作到這一點。筆者使用得最多的就是C++和Python,C++中的一些複雜的點,好比模板(泛型編程)、設計模式(好比template method),在Python中使用起來很是天然。我也看到過有一些文章指出,設計模式每每是特定靜態語言的補丁 -- 爲了彌補語言的缺陷或者限制。服務器

以筆者的知識水平,遠遠不足以評價動態語言與靜態語言的優劣。本文也只是記錄在我使用Python這門動態語言的時候,因爲語言的靈活性,因爲動態類型,踩過的坑,一點思考,以及困惑。app

 

第一個問題:Mutable對象被誤改

這個是在線上環境出現過的一個BUG框架

過後提及來很簡單,服務端數據(放在dict裏面的)被意外修改了,但查證的時候也花了許多時間,僞代碼以下:python2.7

 

上述的代碼很簡單,dct是一個dict,極大機率會調用一個不用修改dct的子函數,極小機率出會調用到可能修改dct的子函數。問題就在於,調用routine函數的參數是服務端全局變量,理論上是不能被修改的。固然,上述的代碼簡單到一眼就能看出問題,但在實際環境中,調用鏈有七八層,並且,在routine這個函數的doc裏面,聲明不會修改dct,該函數自己確實沒有修改dct,但調用的子函數或者子函數的子函數沒有遵照這個約定。

 

從python語言特性看這個問題

本小節解釋上面的代碼爲何會出問題,簡單來講兩點:dict是mutable對象; dict實例做爲參數傳入函數,而後被函數修改了。

  Python中一切都是對象(evething is object),不論是int str dict 仍是類。好比 a =5, 5是一個整數類型的對象(實例);那麼a是什麼,a是5這個對象嗎? 不是的,a只是一個名字,這個名字暫時指向(綁定、映射)到5這個對象。b = a  是什麼意思呢, 是b指向a指向的對象,即a, b都指向整數5這個對象

  那麼什麼是mutable 什麼是immutable呢,mutable是說這個對象是能夠修改的,immutable是說這個對象是不可修改的(廢話)。仍是看Python官方怎麼說的吧

Mutable objects can change their value but keep their id().

  Immutable:An object with a fixed value. Immutable objects include numbers, strings and tuples. Such an object cannot be altered. A new object has to be created if a different value has to be stored. They play an important role in places where a constant hash value is needed, for example as a key in a dictionary.

承接上面的例子(a = 5),int類型就是immutable,你可能說不對啊,好比對a賦值, a=6, 如今a不是變成6了嗎?是的,a如今"變成"6了,但本質是a指向了6這個對象 -- a再也不指向5了

  檢驗對象的惟一標準是id,id函數返回對象的地址,每一個對象在都有惟一的地址。看下面兩個例子就知道了

>>> a = 5;id(a)

  35170056

  >>> a = 6;id(a)

  35170044

 

  >>> lst = [1,2,3]; id(lst)

  39117168

  >>> lst.append(4); id(lst)

  39117168

或者這麼說,對於非可變對象,在對象的生命週期內,沒有辦法改變對象所在內存地址上的值。

  python中,不可變對象包括:int, long, float, bool, str, tuple, frozenset;而其餘的dict list 自定義的對象等屬於可變對象。注意: str也是不可變對象,這也是爲何在多個字符串鏈接操做的時候,推薦使用join而不是+

  並且python沒有機制,讓一個可變對象不可被修改(此處類比的是C++中的const)

 

dict是可變對象!

那在python中,調用函數時的參數傳遞是什麼意思呢,是傳值、傳引用?事實上都不正確,我不清楚有沒有專業而統一的說法,但簡單理解,就是形參(parameter)和實參(argument)都指向同一個對象,僅此而已。來看一下面的代碼:

能夠看到,剛進入子函數double的時候,a,v指向的同一個對象(相同的id)。對於test int的例子,v由於v*=2,指向了另一個對象,但對實參a是沒有任何影響的。對於testlst的時候,v*=2是經過v修改了v指向的對象(也是a指向的對象),所以函數調用完以後,a指向的對象內容發生了變化。

 

如何防止mutable對象被函數誤改:

爲了防止傳入到子函數中的可變對象被修改,最簡單的就是使用copy模塊拷貝一份數據。具體來講,包括copy.copy, copy.deepcopy, 前者是淺拷貝,後者是深拷貝。兩者的區別在於:

簡單來講,深拷貝會遞歸拷貝,遍歷任何compound object而後拷貝,例如:

>>> lst = [1, [2]]
  >>> import copy
  >>> lst1 = copy.copy(lst)
  >>> lst2 = copy.deepcopy(lst)
  >>> print id(lst[1]), id(lst1[1]), id(lst2[1])
  4402825264 4402825264 4402988816
  >>> lst[1].append(3)
  >>> print lst, lst1,lst2
  [1, [2, 3]] [1, [2, 3]] [1, [2]]

 

從例子能夠看出淺拷貝的侷限性,Python中,對象的基本構造也是淺拷貝,例如 dct = {1: [1]}; dct1 = dict(dct)

  正是因爲淺拷貝與深拷貝本質上的區別,兩者性能代價差別很是之大,即便對於被拷貝的對象來講毫無差別:

 

在上面的示例中,dct這個dict的values都是int類型,immutable對象,由於不管淺拷貝 深拷貝效果都是同樣的,可是耗時差別巨大。若是在dct中存在自定義的對象,差別會更大

  那麼爲了安全起見,應該使用深拷貝;爲了性能,應該使用淺拷貝。若是compound object包含的元素都是immutable,那麼淺拷貝既安全又高效,but,對於python這種靈活性極強的語言,極可能某天某人就加入了一個mutable元素。

 

第二個問題:參數檢查

上一節說明沒有簽名 對 函數調用者是多麼不爽,而本章節則說明沒有簽名對函數提供者有多麼不爽。沒有類型檢查真的蛋疼,我也遇到過有人爲了方便,給一個約定是int類型的形參傳入了一個int的list,而可怕的是代碼不報錯,只是表現不正常。

def func(arg):     
 if arg:        
   print 'do lots of things here'    
 else:        
   print 'do anothers'

上述的代碼很糟糕,根本無法「望名知意」,也看不出有關形參 arg的任何信息。但事實上這樣的代碼是存在的,並且還有比這更嚴重的,好比掛羊頭賣狗肉。

  這裏有一個問題,函數指望arg是某種類型,是否應該寫代碼判斷呢,好比:isinstance(arg, str)。由於沒有編譯器靜態來作參數檢查,那麼要不要檢查,如何檢查就徹底是函數提供者的事情。若是檢查,那麼影響性能,也容易違背python的靈活性 -- duck typing; 不檢查,又容易被誤用。

  但在這裏,考慮的是另外一個問題,看代碼的第二行: if arg。python中,幾乎是一切對象均可以看成布爾表達式求值,即這裏的arg能夠是一切python對象,能夠是bool、int、dict、list以及任何自定義對象。不一樣的類型爲「真」的條件不同,好比數值類型(int float)非0即爲真;序列類型(str、list、dict)非空即爲真;而對於自定義對象,在python2.7種則是看是否認義了__nonzero__ 、__len__,若是這兩個函數都沒有定義,那麼實例的布爾求值必定返回真。


 

總結

以上兩個問題,是我使用Python語言以來遇到的諸多問題之二,也是我在同一個地方跌倒過兩次的問題。Python語言以開發效率見長,可是我以爲須要良好的規範才能保證在大型線上項目中使用。並且,我也傾向於假設:人是不可靠的,不會永遠遵照擬定的規範,不會每次修改代碼以後更新docstring ...

  所以,爲了保證代碼的可持續發展,須要作到如下幾點

  第一:擬定並遵照代碼規範

  代碼規範最好在項目啓動時就應該擬定好,能夠參照PEP8和google python styleguild。不少時候風格沒有優劣之說,可是保證項目內的一致性很重要。並保持按期review、對新人review!

  第二:靜態代碼分析

  只要能靜態發現的bug不要放到線上,好比對參數、返回值的檢查,在python3.x中可使用註解(Function Annotations),python2.x也能夠自行封裝decorator來作檢查。對代碼行爲,既可使用Coverity這種高大上的商業軟件,或者王垠大神的Pysonar2,也可使用ast編寫簡單的檢查代碼。

  第三:單元測試

  單元測試的重要性想必你們都知道,在python中出了官方自帶的doctest、unittest,還有許多更強大的框架,好比nose、mock。

  第四:100%的覆蓋率測試

  對於python這種動態語言,出了執行代碼,幾乎沒有其餘比較好的檢查代碼錯誤的手段,因此覆蓋率測試是很是重要的。可使用python原生的sys.settrace、sys.gettrace,也可使用coverage等跟更高級的工具。

原文出處:http://www.cnblogs.com/xybaby/p/7208496.html

 



識別圖中二維碼,領取python全套視頻資料

相關文章
相關標籤/搜索