Python中的一些“坑”

作者:chen_h
微信号 & QQ:862251340
微信公众号:coderpai


1. 不要使用可变对象作为函数默认值

先来看个例子:

def append_to_list(value, def_list = []):
  def_list.append(value)
  return def_list

my_list = append_to_list(1)
# my_list = [1]

my_other_list = append_to_list(2)
# my_other_list = [1, 2]
# 注意,其实我们只想生成一个 [2] 列表,但是却把第一次的结果带进来了,生成了一个 [1, 2] 列表。

import time
def report_arg(my_default = time.time()):
  print my_default

report_arg()
# 1474782900.9

time.sleep(5)

report_arg()
# 1474782900.9
# 两次执行,时间隔了5秒,但是输出时间都没有改变。

这些例子说明了什么?字典,集合,列表等等对象是不适合作为函数默认值的。因为这个默认值是在函数建立的时候已经生成了,每次调用都是使用了这个对象的“缓存”。

可以这样修改代码,如下:

def append_to_list(element, to = None):
  if to is None:
    to = []
  to.append(element)
  return to

2. 生成器不保留迭代过后的结果

代码如下:

gen = ( i  for i in range(10))

2 in gen
# True

5 in gen
# True

1 in gen
# False
# 为什么 1 不在 gen 里面了?因为在调用 2 in gen 这个命令时, 这个时候 1 已经不在这个迭代器里面了,被按需生成过了。

# 如果你还要保留以前的值,那么可以如下操作
gen = ( i  for i in range(10))

a_list = list(gen)
# 可以转换成列表,也可以转换成元祖。

2 in a_list
# True

5 in a_list
# True

1 in a_list
# True
# 就算循环过, 值还在

3. lambda 在闭包中会保存局部变量

这是问题以前一直想不明白,今天看到了一个比较好的解释,现在整理一下。先看一段代码:

my_list = [ lambda : n for n in range(5) ]
for x in my_list:
  print x()

# output
4
4
4
4
4

如果你写这段代码,本意是想输出0,1,2,3,4,但是结果却输出了4,4,4,4,4。要解决这个问题,我们可以将代码修改如下:

# 坚持修改成 list
my_list = [ lambda n = i: n for n in range(5) ]
for x in my_list:
  print x()

# output
0
1
2
3
4

# 修改成生成器
my_list = ( lambda n = i: n for n in range(5) )
for x in my_list:
  print x()

# output
0
1
2
3
4

这是为什么呢?其实,你可以联想一下函数中关于用 list 作为函数参数默认值的问题,如下:

def add(num, l = []):
  l.append(num)
  return l

l1 = add(1)
l2 = add(2)
print 'l1 = ', l1
print 'l2 = ', l2

# output
l1 = [1, 2]
l2 = [1, 2]

这个修改方案也非常简单,不要使用可变对象(列表)作为函数默认值,修改如下:

def add(num, l = None):
  if l is None:
    l = []
  l.append(num)
  return l

l1 = add(1)
l2 = add(2)
print 'l1 = ', l1
print 'l2 = ', l2

# output
l1 = [1]
l2 = [2]

关于这个问题的解释是函数参数默认值在函数定义的时候就已经被创建了,等到函数运行时我们只是在多次的引用同一个变量。为什么要先说明这个问题呢?因为 lambda 就是一个匿名函数。比如:

def func(x):
  return x

# 等价于
func = lambda x:x

所以在函数中的默认值问题在 lambda 中也是同样存在的,当然也有同样的解决方法。我们再来看最初的问题,我们把问题的形式转换一下,如下:

my_list = [ lambda : n for n in range(5) ]
for x in my_list:
  print x()

# 等价于
my_list = []
for n in range(5):
  my_list.append(lambda : n)
for x in my_list:
  print x()

我们在 my_list.append(lambda : n) 中定义的 lambda,其中的 n 是引用 for n in range(5) 这一句中的,这只是 lambda 的定义阶段,lambda 并没有执行,等这两句执行完之后,n 已经等于 4 了,也就是说,我们定义的这5个lambda全部变成了 lambda : 4,等到执行的时候自然输出就成了4,4,4,4,4。我们把上面利用列表的解决方案再换一种形式写一遍,如下:

my_list = []
for i in range(5):
  my_list.append(lambda n = i: n)
for x in my_list:
  print x()

其实,函数和lambda的本质是一样的,那么lambda重的参数默认值的效果应该和函数中的参数默认值的效果也是一样的,函数中的参数默认值是在定义的时候创建并保存的,那么lambda中的参数默认值也一定是一样的。所以这5个lambda有了各自不同的参数默认值,而不是去引用同一个。

那么,这又有一个新的问题,就是函数中的变量都是在什么时候分析引用的。先来做一个简单的实验,如下:

def func(num, l = x):
  l.append(num)
  return l

以上函数如果是在交互模式下输入的,应该在函数输入完毕后马上报错,告诉你 x 没有定义,我们再来看看另一种情况,如下:

def func(n):
  print x

这个函数输入完毕之后,同样是 x 没有定义,系统没有马上报错,但是当你调用的时候才会报错。

这样问题就很明显了,函数(包括lambda)中的默认参数会在函数定义的时候创建或者引用,而函数体内的变量则要等到调用这个函数时才会被创建或者引用。

至此,解释完毕。


4. 在循环中修改列表项

代码如下:

a = [1, 2, 3, 4, 5]
for i in a:
  if not i % 2:
    a.remove(i)

# output
a = [1, 3, 5]
# 结果正常

b = [2, 4, 5, 6]
for i in b:
  if not i % 2:
    b.remove(i)

# output
b = [4, 5]
# 本想去除所有偶数,但显然不对

思考一下,为什么不对?因为当你remove的时候,你影响了列表的index。如下代码更让你明白:

b = [2, 4, 5, 6]
for index, item in enumerate(b):
  print index, item
  if not item % 2:
    print item
    b.remove(item)

# output
(0, 2) # 这里没有问题,2 被删除了
(1, 5) # 因为2被删除了,目前的列表是[4, 5, 6],所有索引list[1]直接去找5,忽略了4
(2, 6) # 6 被删除了

所以,在循环中不要随意修改列表项。


5. 重用全局变量

代码如下:

def my_func():
  # 我们先调用一个未定义的变量
  print var

var = 'global' # 函数之后赋值
# 反正只要调用函数时候,变量被定义了就可以了。
my_func()
# output
global

def my_func():
  var = 'local changed'

varr = 'global'
my_func()
print var

# output
global
# 我们发现,局部变量没有影响到全局变量。

def my_func():
  print var # 虽然你全局设置了这个变量,但是局部变量有同名的,python以为你忘记了定义本地变量了
  var = 'local changed'

var = 'global'
my_func()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-67-d82eda95de40> in <module>()
----> 1 my_func()

<ipython-input-65-0ad11d690936> in my_func()
      1 def my_func():
----> 2         print(var)
      3         var = 'locally changed'
      4

UnboundLocalError: local variable 'var' referenced before assignment

def my_func():
  global var # 这个时候就要加上全局了
  print var
  var = 'local changed'

var = 'global'
my_func()
# output
global
print var
# output
local changed # 因为使用了global,就改变了全局变量。

6. 拷贝可变对象

代码如下:

my_list = [[1,2,3]] * 2
# output
my_list = [[1,2,3], [1,2,3]]

my_list[0][0] = 'a' # 我只修改了子列表中的一项
# output
my_list = [['a',2,3], ['a',2,3]] # 但是都影响到了

# 用这种循环生成不同对象的方法就不影响了
my_list = [ [1,2,3] for i in range(2)]
my_list[0][0] = 'a'
# output
my_list = [['a',2,3], [1,2,3]]

7. 列表的 + 和 +=,append和extend

首先说明,id函数可以获得对象的内存地址,如果两个对象的内存地址是一样的,那么这两个对象肯定是一个对象。

list = []
print 'ID: ', id(list)
# output
ID: 1234567890

list += [1]
print 'ID: ', id(list)
# ID: 1234567890
# 使用 += ,还是在原来的列表上操作

list = list + [2]
print 'ID: ', id(list)
# ID: 9876543210
# 使用 + ,其实已经改变了原有列表

list = []
print 'ID: ', id(list)
# output
# 'ID'1212121212

list.append(1)
print 'ID: ', id(list)
# output
# ID: 1212121212
# append 是在原来列表上面添加

list.extend([2])
print 'ID: ', id(list)
# output
# ID: 1212121212
# extend 也是在原来列表上面添加

8. bool 其实是 int 的子类

代码如下:

True + True
# output
2

3 * True
# output
3

True << 10
# output
1024

猜你喜欢

转载自blog.csdn.net/coderpai/article/details/80420725