最近使用Python的過程中遇到了一些坑,例如用datetime.datetime.now()這個可變物件作為函數的預設參數,模組循環依賴等等。
在此記錄一下,方便以後查詢和補充。
在使用函數的過程中,經常會涉及預設參數。在Python中,當使用可變物件作為預設參數的時候,就可能產生非預期的結果。
下面看一個例子:
def append_item(a = 1, b = []): b.append(a) print b append_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 b append_item(a=1) append_item(a=3) append_item(a=5)
結果為:
[1] [3] [5]
Python的作用域解析順序為Local、Enclosing、Global、Built-in,也就是說Python解釋器會依照這個順序解析變數。
看一個簡單的例子:
global_var = 0 def outer_func(): outer_var = 1 def inner_func(): inner_var = 2 print "global_var is :", global_var print "outer_var is :", outer_var print "inner_var is :", inner_var inner_func() outer_func()
結果為:
global_var is : 0 outer_var is : 1 inner_var is : 2
在Python中,關於作用域有一點要注意的是,在一個作用域裡面給一個變數賦值的時候,Python會認為這個變數是目前作用域的本地變數。
對於這一點也是比較容易理解的,對於下面程式碼var_func中給num變數進行了賦值,所以這裡的num就是var_func作用域的本地變數。
num = 0 def var_func(): num = 1 print "num is :", num var_func()
問題一
但是,當我們透過下面的方式使用變數的時候,就會產生問題了:
num = 0 def var_func(): print "num is :", num num = 1 var_func()
結果如下:
UnboundLocalError: local variable 'num' referenced before assignment
之所以產生這個錯誤,就是因為我們在var_func中給num變數進行了賦值,所以Python解釋器會認為num是var_func作用域的本地變量,但是當程式碼執行到print "num is :", num語句的時候,num還是未定義。
問題二
上面的錯誤還是比較明顯的,還有一個比較隱藏的錯誤形式如下:
li = [1, 2, 3] def foo(): li.append(4) print li foo() def bar(): li +=[5] print li bar()
程式碼的結果為:
[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 li foo() def bar(): global li li +=[5] print li bar()
在Python中,有類別屬性和實例屬性。類別屬性是屬於類別本身的,被所有的類別實例共用。
類別屬性可以透過類別名稱存取和修改,也可以透過類別實例進行存取和修改。但是,當實例定義了跟類別同名的屬性後,類別屬性就被隱藏了。
看下面這個範例:
class Student(object): books = ["Python", "JavaScript", "CSS"] def init(self, name, age): self.name = name self.age = age pass wilber = Student("Wilber", 27) print "%s is %d years old" %(wilber.name, wilber.age) print Student.books print wilber.books wilber.books = ["HTML", "AngularJS"] print Student.books print wilber.books del wilber.books print 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中,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 38639896
在对Python对象进行赋值的操作中,一定要注意对象的深浅拷贝,一不小心就可能踩坑了。
当使用下面的操作的时候,会产生浅拷贝的效果:
使用切片[:]操作
使用copy模块中的copy()函数
使用copy模块里面的浅拷贝函数copy():
import copy will = ["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 copy will = ["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_y def inc_count(): module_y.count += 1 print module_y.count # module_y.py import module_x count = 10 def run(): module_x.inc_count() run()
其实,在编码的过程中就应当避免循环依赖的情况,或者代码重构的过程中消除循环依赖。
当然,上面的问题也是可以解决的,常用的解决办法就是把引用关系搞清楚,让某个模块在真正需要的时候再导入(一般放到函数里面)。
对于上面的例子,就可以把module_x.py修改为如下形式,在函数内部导入module_y:
# module_x.py def inc_count(): import module_y module_y.count += 1
以上是詳解開發中常遇到的Python陷阱與注意點的詳細內容。更多資訊請關注PHP中文網其他相關文章!