原文發表在我的博客主頁,轉載請注明出處!
建議二十八:區別對待可變對象和不可變對象
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的地址,這時我們在將可變對象作為默認參數的時候要警惕的,對可變對象的更改會直接影響原對象,可以用如下方式解決:
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]
列表解析的使用非常靈活:
- 支持多重嵌套,如果需要生成一個二維列表可以使用列表解析嵌套的方式
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
為什么要推薦在需要生成列表的時候使用列表解析呢?
- 使用列表解析更為直觀清晰,代碼更為簡潔
- 列表解析的效率更高,但是對於大數據處理,列表解析並不是一個最佳選擇,過多的內存消耗可能會導致MemoryError
除了列表可以使用列表解析的語法之外,其他內置的數據結構也支持,如下:
#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中函數傳參數有三種觀點:
- 傳值
- 傳引用
- 可變對象傳引用,不可變對象傳值
這些理解都是有些偏差的,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操作使得b與a引用同一對象,而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個建議