使用python時需要注意的點和坑
最近使用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中的作用域
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()
結果如下,之所以產生這個錯誤,就是因為我們在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 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}
tuple是“可變的”
在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的深淺拷貝
在對Python物件進行賦值的操作中,一定要注意物件的深淺拷貝,一不小心就可能踩坑了。
當使用下面的操作的時候,會產生淺拷貝的效果:
- 使用切片[:]操作
- 使用工廠函式(如list/dir/set)
- 使用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
print module_y.count