記錄遇到的Python陷阱和注意點

最近使用Python的過程當中遇到了一些坑,例如用datetime.datetime.now()這個可變對象做爲函數的默認參數,模塊循環依賴等等。python

在此記錄一下,方便之後查詢和補充。app

避免可變對象做爲默認參數

在使用函數的過程當中,常常會涉及默認參數。在Python中,當使用可變對象做爲默認參數的時候,就可能產生非預期的結果。函數

下面看一個例子:ui

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並無被初始化爲[],而是保持了前面函數調用的值。code

之因此獲得這個結果,是由於在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)

結果爲:ip

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解釋器會認爲numvar_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
相關文章
相關標籤/搜索