记录遇到的Python陷阱和注意点
來源:http://www.cnblogs.com/wilber2013/p/5178620.html
最近使用Python的過程中遇到了一些坑,例如用datetime.datetime.now()這個可變對象作為函數的默認參數,模塊循環依賴等等。
在此記錄一下,方便以后查詢和補充。
避免可變對象作為默認參數
在使用函數的過程中,經常會涉及默認參數。在Python中,當使用可變對象作為默認參數的時候,就可能產生非預期的結果。
下面看一個例子:
def append_item(a = 1, b = []):b.append(a)print bappend_item(a=1) append_item(a=3) append_item(a=5)結果為:
[1] [1, 3] [1, 3, 5]從結果中可以看到,當后面兩次調用append_item函數的時候,函數參數b并沒有被初始化為[],而是保持了前面函數調用的值。
之所以得到這個結果,是因為在Python中,一個函數參數的默認值,僅僅在該函數定義的時候,被初始化一次。
下面看一個例子證明Python的這個特性:
class Test(object): def __init__(self): print("Init Test") def arg_init(a, b = Test()): print(a) arg_init(1) arg_init(3) arg_init(5)結果為:
Init Test 1 3 5從這個例子的結果就可以看到,Test類僅僅被實例化了一次,也就是說默認參數跟函數調用次數無關,僅僅在函數定義的時候被初始化一次。
可變默認參數的正確使用
對于可變的默認參數,我們可以使用下面的模式來避免上面的非預期結果:
def append_item(a = 1, b = None):if b is None:b = []b.append(a)print bappend_item(a=1) append_item(a=3) append_item(a=5)結果為:
[1] [3] [5]Python中的作用域
Python的作用域解析順序為Local、Enclosing、Global、Built-in,也就是說Python解釋器會根據這個順序解析變量。
看一個簡單的例子:
global_var = 0def outer_func():outer_var = 1def inner_func():inner_var = 2print "global_var is :", global_varprint "outer_var is :", outer_varprint "inner_var is :", inner_varinner_func()outer_func()結果為:
global_var is : 0 outer_var is : 1 inner_var is : 2在Python中,關于作用域有一點需要注意的是,在一個作用域里面給一個變量賦值的時候,Python會認為這個變量是當前作用域的本地變量。
對于這一點也是比較容易理解的,對于下面代碼var_func中給num變量進行了賦值,所以此處的num就是var_func作用域的本地變量。
num = 0def var_func():num = 1print "num is :", numvar_func()問題一
但是,當我們通過下面的方式使用變量的時候,就會產生問題了:
num = 0def var_func():print "num is :", numnum = 1var_func()結果如下,之所以產生這個錯誤,就是因為我們在var_func中給num變量進行了賦值,所以Python解釋器會認為num是var_func作用域的本地變量,但是當代碼執行到print "num is :", num語句的時候,num還是未定義。
UnboundLocalError: local variable 'num' referenced before assignment問題二
上面的錯誤還是比較明顯的,還有一種比較隱蔽的錯誤形式如下:
li = [1, 2, 3]def foo():li.append(4)print lifoo()def bar():li +=[5]print libar()代碼的結果為:
[1, 2, 3, 4] UnboundLocalError: local variable 'li' referenced before assignment在foo函數中,根據Python的作用域解析順序,該函數中使用了全局的li變量;但是在bar函數中,對li變量進行了賦值,所以li會被當作bar作用域中的變量。
對于bar函數的這個問題,可以通過global關鍵字。
li = [1, 2, 3]def foo():li.append(4)print lifoo()def bar():global lili +=[5]print libar()類屬性隱藏
在Python中,有類屬性和實例屬性。類屬性是屬于類本身的,被所有的類實例共享。
類屬性可以通過類名訪問和修改,也可以通過類實例進行訪問和修改。但是,當實例定義了跟類同名的屬性后,類屬性就被隱藏了。
看下面這個例子:
class Student(object):books = ["Python", "JavaScript", "CSS"]def __init__(self, name, age):self.name = nameself.age = agepasswilber = Student("Wilber", 27) print "%s is %d years old" %(wilber.name, wilber.age)print Student.books print wilber.bookswilber.books = ["HTML", "AngularJS"]print Student.books print wilber.booksdel wilber.booksprint Student.books print wilber.books代碼的結果如下,起初wilber實例可以直接訪問類的books屬性,但是當實例wilber定義了名稱為books的實例屬性之后,wilber實例的books屬性就“隱藏”了類的books屬性;當刪除了wilber實例的books屬性之后,wilber.books就又對應類的books屬性了。
Wilber is 27 years old ['Python', 'JavaScript', 'CSS'] ['Python', 'JavaScript', 'CSS'] ['Python', 'JavaScript', 'CSS'] ['HTML', 'AngularJS'] ['Python', 'JavaScript', 'CSS'] ['Python', 'JavaScript', 'CSS']當在Python值使用繼承的時候,也要注意類屬性的隱藏。對于一個類,可以通過類的__dict__屬性來查看所有的類屬性。
當通過類名來訪問一個類屬性的時候,會首先查找類的__dict__屬性,如果沒有找到類屬性,就會繼續查找父類。但是,如果子類定義了跟父類同名的類屬性后,子類的類屬性就會隱藏父類的類屬性。
看一個例子:
class A(object):count = 1 class B(A):pass class C(A):pass print A.count, B.count, C.count B.count = 2 print A.count, B.count, C.count A.count = 3 print A.count, B.count, C.count print B.__dict__ print C.__dict__結果如下,當類B定義了count這個類屬性之后,就會隱藏父類的count屬性:
1 1 1 1 2 1 3 2 3 {'count': 2, '__module__': '__main__', '__doc__': None} {'__module__': '__main__', '__doc__': None}一般來說,在Python中,類實例屬性的訪問規則算是比較直觀的。
? ? ? 但是,仍然存在一些不是很直觀的地方,特別是對C++和Java程序員來說,更是如此。
? ? ? 在這里,我們需要明白以下幾個地方:
? ? ? 1.Python是一門動態語言,任何實體都可以動態地添加或刪除屬性。
? ? ? 2.一個類定義了一個作用域。
? ? ? 3.類實例也引入了一個作用域,這與相應類定義的作用域不同。
? ? ? 4.在類實例中查找屬性的時候,首先在實例自己的作用域中查找,如果沒有找到,則再在類定義的作用域中查找。
? ? ? 5.在對類實例屬性進行賦值的時候,實際上會在類實例定義的作用域中添加一個屬性(如果還不存在的話),并不會影響到相應類中定義的同名屬性。
? ? ? 下面看一個例子,加深對上述幾點的理解:
class A:cls_i = 0cls_j = {}def __init__(self):self.instance_i = 0self.instance_j = {} 在這里,我們先定義類A的一個實例a,然后再看看類A的作用域和實例a的作用域中分別有什么:>>> a = A() >>> a.__dict__ {'instance_j': {}, 'instance_i': 0} >>> A.__dict__ {'__init__': , '__module__': '__main__', 'cls_i': 0, 'cls_j': {}, '__doc__': None}
我們看到,a的作用域中有instance_i和instance_j,A的作用域中有cls_i和cls_j。
? ? ? 我們再來看看名字查找是如何發生的:
>>> a.cls_i 0 >>> a.instance_i 0 在查找cls_i的時候,實例a的作用域中是沒有它的,卻在A的作用域中找到了它;在查找instance_i的時候,直接可在a的作用域中找到它。如果我們企圖通過實例a來修改cls_i的值,那會怎樣呢: >>> a.cls_i = 1 >>> a.__dict__ {'instance_j': {}, 'cls_i': 1, 'instance_i': 0} >>> A.__dict__ {'__init__': , '__module__': '__main__', 'cls_i': 0, 'cls_j': {}, '__doc__': None}我們可以看到,a的作用域中多了一個cls_i屬性,其值為1;同時,我們也注意到A作用域中的cls_i屬性的值仍然為0;在這里,我們其實是增加了一個實例屬性,并沒有修改到類屬性。
? ? ? ?如果我們通過實例a操縱cls_j中的數據(注意不是cls_j本身),又會怎么樣呢:
>>> a.cls_j['a'] = 'a' >>> a.__dict__ {'instance_j': {}, 'cls_i': 1, 'instance_i': 0} >>> A.__dict__ {'__init__': , '__module__': '__main__', 'cls_i': 0, 'cls_j': {'a': 'a'}, '__doc__': None}我們可以看到a的作用域沒有發生什么變化,但是A的作用域發生了一些變化,cls_j中的數據發生了變化。
? ? ? 實例的作用域發生變化,并不會影響到該類的其它實例,但是類的作用域發生變化,則會影響到該類的所有實例,包括在這之前創建的實例:
>>> A.cls_k = 0 >>> i.cls_k 0http://www.cnblogs.com/frydsh/p/3194710.htmltuple是“可變的”
在Python中,tuple是不可變對象,但是這里的不可變指的是tuple這個容器總的元素不可變(確切的說是元素的id),但是元素的值是可以改變的。
tpl = (1, 2, 3, [4, 5, 6]) print id(tpl) print id(tpl[3]) tpl[3].extend([7, 8]) print tpl print id(tpl) print id(tpl[3])代碼結果如下,對于tpl對象,它的每個元素都是不可變的,但是tpl[3]是一個list對象。也就是說,對于這個tpl對象,id(tpl[3])是不可變的,但是tpl[3]確是可變的。
36764576 38639896 (1, 2, 3, [4, 5, 6, 7, 8]) 36764576 38639896Python的深淺拷貝
在對Python對象進行賦值的操作中,一定要注意對象的深淺拷貝,一不小心就可能踩坑了。
當使用下面的操作的時候,會產生淺拷貝的效果:
- 使用切片[:]操作
- 使用工廠函數(如list/dir/set)
- 使用copy模塊中的copy()函數
使用copy模塊里面的淺拷貝函數copy():
import copywill = ["Will", 28, ["Python", "C#", "JavaScript"]] wilber = copy.copy(will)print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]will[0] = "Wilber" will[2].append("CSS") print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]使用copy模塊里面的深拷貝函數deepcopy():
import copywill = ["Will", 28, ["Python", "C#", "JavaScript"]] wilber = copy.deepcopy(will)print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]will[0] = "Wilber" will[2].append("CSS") print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]模塊循環依賴
在Python中使用import導入模塊的時候,有的時候會產生模塊循環依賴,例如下面的例子,module_x模塊和module_y模塊相互依賴,運行module_y.py的時候就會產生錯誤。
# module_x.py import module_ydef inc_count():module_y.count += 1print module_y.count# module_y.py import module_xcount = 10def run():module_x.inc_count()run()其實,在編碼的過程中就應當避免循環依賴的情況,或者代碼重構的過程中消除循環依賴。
當然,上面的問題也是可以解決的,常用的解決辦法就是把引用關系搞清楚,讓某個模塊在真正需要的時候再導入(一般放到函數里面)。
對于上面的例子,就可以把module_x.py修改為如下形式,在函數內部導入module_y:
# module_x.py def inc_count():import module_ymodule_y.count += 1print module_y.count總結
以上是生活随笔為你收集整理的记录遇到的Python陷阱和注意点的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MAC OS 命令行使用详解
- 下一篇: 在一个二维数组中(每个一维数组的长度相同