首頁  >  文章  >  後端開發  >  開發中常遇到的Python陷阱與注意點

開發中常遇到的Python陷阱與注意點

高洛峰
高洛峰原創
2016-10-17 12:00:31841瀏覽

最近使用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中的作用域

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()

Python中的作用域

global_var is : 0
outer_var is : 1
inner_var is : 2

Python中的作用域


num = 0
def var_func():
    num = 1
    print "num is :", num
     
var_func()

的作用域解析順序為Local、Enclosing、Global、Built-in,也就是說Python解釋器會依照這個順序解析變數。

看一個簡單的例子:

num = 0
def var_func():
    print "num is :", num
    num = 1
     
var_func()

結果為:

UnboundLocalError: local variable 'num' referenced before assignment

在Python中,關於作用域有一點要注意的是,

li = [1, 2, 3]
def foo():
    li.append(4)
    print li
foo()
def bar():
    li +=[5]
    print li
bar()

在Python中,關於作用域有一點要注意的是,在一個作用域的時候給一個變數Python會認為這個變數是目前作用域的本地變數。


對於這一點也是比較容易理解的,對於下面程式碼var_func中給num變數進行了賦值,所以此處的num就是var_func作用域的本地變數。

[1, 2, 3, 4]
UnboundLocalError: local variable 'li' referenced before assignment

問題一

但是,當我們透過下面的方式使用變數的時候,就會產生問題了:

li = [1, 2, 3]
def foo():
    li.append(4)
    print li
     
foo()
def bar():
    global li
    li +=[5]
    print li
     
bar()

結果如下:

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

之所以產生這個錯誤,就是因為我們在var_func中給num變量進行了賦值,所以Python解釋器會認為num是var_func作用域的本地變量,但是當程式碼執行到print "num is :", num語句的時候,num還是未定義。


問題二

上面的錯誤還是比較明顯的,還有一種比較隱蔽的錯誤形式如下:

Wilber is 27 years old
['Python', 'JavaScript', 'CSS']
['Python', 'JavaScript', 'CSS']
['Python', 'JavaScript', 'CSS']
['HTML', 'AngularJS']
['Python', 'JavaScript', 'CSS']
['Python', 'JavaScript', 'CSS']

程式碼的結果為:

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__

在Pytoohon函數中,根據順序解析的作用域,函數中使用了全域的li變數;但是在bar函數中,對li變數進行了賦值,所以li會被當作bar作用域中的變數。


對於bar函數的這個問題,可以透過global來關鍵字。

1 1 1
1 2 1
3 2 3
{'count': 2, '__module__': '__main__', '__doc__': None}
{'__module__': '__main__', '__doc__': None}

類別屬性隱藏

在Python中,有類別屬性和實例屬性。類別屬性是屬於類別本身的,被所有的類別實例共用。

類別屬性可以透過類別名稱存取和修改,也可以透過類別實例進行存取和修改。但是,當實例定義了跟類別同名的屬性後,類別屬性就被隱藏了。

🎜🎜🎜看下面這個例子:🎜
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])
🎜程式碼的結果如下,起初wilber實例可以直接存取類別的books屬性,但是當實例wilber定義了名稱為books的實例屬性之後,wilber實例的books屬性就「隱藏」了類別的books屬性;當刪除了wilber實例的books屬性之後,wilber.books就又對應類別的books屬性了。 🎜
36764576
38639896
(1, 2, 3, [4, 5, 6, 7, 8])
36764576
38639896
🎜當在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

   


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn