編寫高質量代碼--改善python程序的建議(六)

原文發表在個人博客主頁,轉載請註明出處!python

建議二十八:區別對待可變對象和不可變對象
python中一切皆對象,每個對象都有一個惟一的標識符(id())、類型(type())以及值,對象根據其值可否修改分爲可變對象和不可變對象,其中數字、字符串、元組屬於不可變對象,字典以及列表、字節數組屬於可變對象。
來看一段程序:數組

class Student(object):
    def __init__(self,name,course=[]):
        self.name = name
        self.course = course

    def addcourse(self,coursename):
        self.course.append(coursename)

    def printcourse(self):
        for item in self.course:
            print item

xl = Student('xl')
xl.addcourse('computer')
xl.addcourse('automation')
print xl.name + "'s course:"
xl.printcourse()
print "~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
cyj = Student('cyj')
cyj.addcourse('software')
cyj.addcourse('NLP')
print cyj.name + "'s course:"
cyj.printcourse()

運行結果會讓初學者大吃一驚:數據結構

xl's course:
computer
automation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cyj's course:
computer
automation
software
NLP

經過查看xl和cyj的course變量的id,發現他們的值是同樣的,即指向內存中的同一塊地址,可是xl和cyj倒是兩個不一樣的對象。在實例化這兩個對象的時候,這兩個對象被分配了不一樣的內存空間,而且調用init()函數進行初始化,但因爲init()函數的第二個參數是個默認參數,默認參數在函數調用的時候僅僅被評估一次,之後都會使用第一次評估的結果,所以實際上對象空間裏面course所指向的是list的地址,這時咱們在將可變對象做爲默認參數的時候要警戒的,對可變對象的更改會直接影響原對象,能夠用以下方式解決:app

def __init__(self,name,course=None):
        self.name = name
        if course is None:course = []
        self.course = course

對於不可變對象來講,當咱們對其進行相關操做的時候,python實際上仍然保存原來的值,從新建立一個新的對象。當有兩個對象同時指向一個字符串對象的時候,對其中一個對象的操做並不會影響另外一個對象。好比:函數

str1 = "write pythonic code"
str2 = str1
str1 = str1[-4:]
print id(str1)
print id(str2)
print str1
print str2

建議二十九:和{}:一致性容器初始化形式
列表是一個頗有用的數據結構,因爲其靈活性在實際應用中被普遍使用。對於列表來講,列表解析十分經常使用。
列表解析的語法以下,它迭代iterable中的每個元素,當條件知足的時候便根據表達式expr計算的內容生成一個元素並放入新的列表中,依次類推,最終返回整個列表。大數據

[expr for iter_item in iterable if cond_expr]

列表解析的使用很是靈活:ui

  • 支持多重嵌套,若是須要生成一個二維列表可使用列表解析嵌套的方式
nested_list = [['Hello', 'World'],['Goodbye', 'World']]
nested_list =  [[ele.upper() for ele in word] for word in nested_list]
  • 支持多重迭代
[(a,b) for a in ['1', '2', '3', '4'] for b in ['a', 'b', 'c', 'd'] if a != b]
  • 表達式能夠是簡單表達式,也能夠是複雜表達式,甚至是函數
def f(v):
    if v%2 == 0:
        v = v ** 2
    else:
        v = v + 1
    return v
print [f(v) for v in [1,2,3,-1] if v > 0]
print [v ** 2 if v %2 == 0 else v + 1 for v in [1,2,3,-1] if v > 0]
  • iterable能夠是任意可迭代對象
fp = open('wdf.py','r')
res = [i for i in fp if 'weixin' in i]
print res

爲何要推薦在須要生成列表的時候使用列表解析呢?debug

  • 使用列表解析更爲直觀清晰,代碼更爲簡潔
  • 列表解析的效率更高,可是對於大數據處理,列表解析並非一個最佳選擇,過多的內存消耗可能會致使MemoryError

除了列表可使用列表解析的語法以外,其餘內置的數據結構也支持,以下:code

#generator
(expr for iter_item in iterable if cond_expr)
#set
{expr for iter_item in iterable if cond_expr}
#dict
{expr1: expr2 for iter_item in iterable if cond_expr}

建議三十:記住函數傳參既不是傳值也不是傳引用
以往關於python中函數傳參數有三種觀點:orm

  • 傳值
  • 傳引用
  • 可變對象傳引用,不可變對象傳值

這些理解都是有些誤差的,python中的賦值與咱們所理解的C/C++等語言的賦值的意思並不同。以以下語句爲例來看C/C++和python是如何運做的

a = 5, b= a, b = 7

C/C++中當執行b=a的時候,在內存中申請一塊內存並將a的值複製到該內存中,當執行b=7以後是將b對應的值從5修改到7
python中賦值並非複製,b=a操做使得ba引用同一對象,而b=7則是將b指向對象7
所以,對於python函數參數既不是傳值也不是傳引用,應該是傳對象或者說傳對象的引用。函數參數在傳遞的過程當中將整個對象傳入,對可變對象的修改在函數外部以及內部均可見,調用者和被調用者之間共享這個對象,而對於不可變對象,因爲並不能真正被修改,所以,修改每每是經過生成一個新對象而後賦值來實現的。


建議三十一:慎用變長參數
python支持可變長度的參數列表,能夠經過在函數定義的時候使用*args和**kwargs這兩個特殊語法來實現。
使用args來實現可變參數列表:args用於接收一個包裝爲元組形式的參數列表來傳遞非關鍵字參數,參數個數能夠任意:

def sumf(*args):
    res = 0
    for x in args[:]:
        res += x
    return res

print sumf(2,3,4)
print sumf(1,2,3,4,5)

使用**kwargs接收字典形式的關鍵字參數列表,其中字典的鍵值分別表示不可變參數的參數名和值:

def category_table(**kwargs):
    for name, value in kwargs.items():
        print '{0} is a kind of {1}'.format(name, value)

category_table(apple = 'fruit', carrot = 'vegetable')

當普通參數,默認參數,和上述兩種參數同時存在的時候,會優先給普通參數和默認參數賦值,爲何要慎用可變長參數呢?

  • 使用過於靈活,是代碼不夠清晰
  • 若是一個函數的參數列表很長,雖然能夠經過使用*args和**kwargs來簡化函數的定義,但一般意味着這個函數能夠有更好的實現方式,應該被重構。
  • 可變長參數適合在下列狀況下使用:
  • 爲函數添加一個裝飾器
  • 若是參數的數目不肯定,能夠考慮使用變長參數
  • 用來實現函數的多態或者在繼承狀況下子類須要調用父類的某些方法的時候

參數三十二:深刻理解str()和repr()的區別
這兩個方法均可以將python中的對象轉換爲字符串,他們的使用以及輸出都很是類似,區別呢?

  • 二者之間的目標不一樣:str()主要面向用戶,其目的是可讀性,返回形式爲用戶友好性和可讀性都較強的字符串類型,而repr()面向python解釋器,或者說開發人員,其目的是準確性,其返回值表示python解釋器的內部函數,經常使用做debug
  • 在解釋器中直接輸入a時默認調用repr()函數,而print a則調用str()函數
  • repr()的返回值通常能夠用eval()函數來還原
obj == eval(repr(obj))
  • 這兩個方法分別調用內建的__str____repr__()方法,通常來講在類中都應該定義後者,而前者方法則爲可選,若是沒有,默認使用後者的結果來返回對象的字符串表示形式

建議三十三:分清staticmethod和classmethod的使用場景
python中的靜態方法(staticmethod)和類方法(classmethod)都依賴於裝飾器來實現,用法以下:

#staticmethod
class C(object):
    @staticmethod
    def f(arg1, arg2, ...):
#classmethod
class C(object):
    @classmethod
    def f(arg1, arg2, ...):

靜態方法和類方法均可以經過類名.方法名或者實例.方法名的形式來訪問。其中靜態方法沒有常規方法的特殊行爲,如綁定、非綁定、隱式參數等規則,而類方法的調用使用類自己做爲其隱含參數,但調用自己並不須要顯示提供該參數。
那爲何須要靜態方法和類方法呢?假設有水果類Fruit,它用屬性total表示總量,用set()來設置重量,print_total()方法來打印水果數量。類Apple和類Orange繼承自Fruit,現須要分別跟蹤不一樣類型的水果的總量,實現方法彙總:

  • 利用普通的實例方法來實現:在Apple和Orange類中分別定義類變量total,而後覆蓋基類的set()和print_total()方法
  • 使用類方法實現
class Fruit(object):
    total = 0
    def print_total(cls):
        print cls.total
    @classmethod
    def set(cls, value):
        cls.total = value
class Apple(Fruit):
    pass
class Orange(Fruit):
    pass
app1 = Apple()
app1.set(200)
app2 = Apple()
org1 = Orange()
org1.set(300)
org2 = Orange()
app1.print_total()
org1.print_total()

簡單分析可知,針對不一樣種類的水果對象調用set()方法的時候隱形傳入的參數爲該對象所對應的類,在調用set()的過程當中動態生成了對應的類的類變量。
靜態方法通常適用於既不跟特定的實例相關也不跟特定的類相關的方法。他存在於類中,較以外部函數可以更加有效的將代碼組織起來,從而使相關代碼的垂直距離更近,提升代碼的可維護性。

參考:編寫高質量代碼--改善python程序的91個建議

相關文章
相關標籤/搜索