记录遇到的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
相关文章
相关标签/搜索