Python陷阱

xiaoxiao2021-02-27  153

最近使用Python的过程中遇到了一些坑,例如用datetime.datetime.now()这个可变对象作为函数的默认参数,模块循环依赖等等。

在此记录一下,方便以后查询和补充。

避免可变对象作为默认参数

在使用函数的过程中,经常会涉及默认参数。在Python中,当使用可变对象作为默认参数的时候,就可能产生非预期的结果。

下面看一个例子:

1 2 3 4 5 6 7 def  append_item(a  =  1 , b  =  []):      b.append(a)      print  b       append_item(a = 1 ) append_item(a = 3 ) append_item(a = 5 )

结果为:

1 2 3 [1] [1, 3] [1, 3, 5]

从结果中可以看到,当后面两次调用append_item函数的时候,函数参数b并没有被初始化为[],而是保持了前面函数调用的值。

之所以得到这个结果,是因为在Python中,一个函数参数的默认值,仅仅在该函数定义的时候,被初始化一次。

下面看一个例子证明Python的这个特性:

1 2 3 4 5 6 7 8 9 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 )

结果为:

1 2 3 4 Init Test 1 3 5

从这个例子的结果就可以看到,Test类仅仅被实例化了一次,也就是说默认参数跟函数调用次数无关,仅仅在函数定义的时候被初始化一次。

可变默认参数的正确使用

对于可变的默认参数,我们可以使用下面的模式来避免上面的非预期结果:

1 2 3 4 5 6 7 8 9 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 2 3 [1] [3] [5]

Python中的作用域

Python的作用域解析顺序为Local、Enclosing、Global、Built-in,也就是说Python解释器会根据这个顺序解析变量。

看一个简单的例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 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()

结果为:

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

在Python中,关于作用域有一点需要注意的是,在一个作用域里面给一个变量赋值的时候,Python会认为这个变量是当前作用域的本地变量。

对于这一点也是比较容易理解的,对于下面代码var_func中给num变量进行了赋值,所以此处的num就是var_func作用域的本地变量。

1 2 3 4 5 6 num  =  0 def  var_func():      num  =  1      print  "num is :" , num       var_func()

问题一

但是,当我们通过下面的方式使用变量的时候,就会产生问题了:

1 2 3 4 5 6 num  =  0 def  var_func():      print  "num is :" , num      num  =  1       var_func()

结果如下:

1 UnboundLocalError:  local  variable  'num'  referenced before assignment

之所以产生这个错误,就是因为我们在var_func中给num变量进行了赋值,所以Python解释器会认为num是var_func作用域的本地变量,但是当代码执行到print "num is :", num语句的时候,num还是未定义。

问题二

上面的错误还是比较明显的,还有一种比较隐蔽的错误形式如下:

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

代码的结果为:

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

在foo函数中,根据Python的作用域解析顺序,该函数中使用了全局的li变量;但是在bar函数中,对li变量进行了赋值,所以li会被当作bar作用域中的变量。

对于bar函数的这个问题,可以通过global关键字。

1 2 3 4 5 6 7 8 9 10 11 12 li  =  [ 1 ,  2 ,  3 ] def  foo():      li.append( 4 )      print  li       foo() def  bar():      global  li      li  + = [ 5 ]      print  li       bar()

类属性隐藏

在Python中,有类属性和实例属性。类属性是属于类本身的,被所有的类实例共享。

类属性可以通过类名访问和修改,也可以通过类实例进行访问和修改。但是,当实例定义了跟类同名的属性后,类属性就被隐藏了。

看下面这个例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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属性了。

1 2 3 4 5 6 7 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__属性,如果没有找到类属性,就会继续查找父类。但是,如果子类定义了跟父类同名的类属性后,子类的类属性就会隐藏父类的类属性。

看一个例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 2 3 4 5 1 1 1 1 2 1 3 2 3 { 'count' : 2,  '__module__' :  '__main__' ,  '__doc__' : None} { '__module__' :  '__main__' ,  '__doc__' : None}

tuple是“可变的”

在Python中,tuple是不可变对象,但是这里的不可变指的是tuple这个容器总的元素不可变(确切的说是元素的id),但是元素的值是可以改变的。

1 2 3 4 5 6 7 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]确是可变的。

1 2 3 4 5 36764576 38639896 (1, 2, 3, [4, 5, 6, 7, 8]) 36764576 38639896

Python的深浅拷贝

在对Python对象进行赋值的操作中,一定要注意对象的深浅拷贝,一不小心就可能踩坑了。

当使用下面的操作的时候,会产生浅拷贝的效果:

使用切片[:]操作

使用工厂函数(如list/dir/set)

使用copy模块中的copy()函数

使用copy模块里面的浅拷贝函数copy():

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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():

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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的时候就会产生错误。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 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:

1 2 3 4 # module_x.py def  inc_count():      import  module_y      module_y.count  + =  1

文章转自:http://www.cnblogs.com/wilber2013/p/5178620.html

转载请注明原文地址: https://www.6miu.com/read-15553.html

最新回复(0)