Yhzhtk / note

知识代码笔记
https://github.com/Yhzhtk/note/issues
MIT License
108 stars 11 forks source link

Python 新手常犯的两个错误 #4

Open Yhzhtk opened 11 years ago

Yhzhtk commented 11 years ago

Python 新手常犯的两个错误

1、用一个可变的值作为默认值

这是一个绝对值得放在第一个来说的问题。不仅仅是因为产生这种BUG的原因很微妙,而且这种问题也很难检查出来。思考一下下面的代码片段:

def foo(numbers=[]):
    numbers.append(9)
    print numbers

在这里,我们定义了一个 list (默认为空),给它加入9并且打印出来。

>>> foo()
[9]
>>> foo(numbers=[1,2])
[1, 2, 9]
>>> foo(numbers=[1,2,3])
[1, 2, 3, 9]

看起来还行吧?可是当我们不输入number 参数来调用 foo 函数时,神奇的事情发生了:

>>> foo() # first time, like before
[9]
>>> foo() # second time
[9, 9]
>>> foo() # third time...
[9, 9, 9]
>>> foo() # WHAT IS THIS BLACK MAGIC?!
[9, 9, 9, 9]

在Python里,函数的默认值实在函数定义的时候实例化的,而不是在调用的时候。在你每次给函数指定一个默认值的时候,Python都会存储这个值。如果在调用函数的时候重写了默认值,那么这个存储的值就不会被使用。当你不重写默认值的时候,那么Python就会让默认值引用存储的值(这个例子里的numbers)。

解决方法:
def foo(numbers=None):
    if numbers is None:
        numbers = []
    numbers.append(9)
    print numbers

2、变量作用域,局部变量和全局变量的问题

通常,当我们定义了一个全局变量(好吧,我这样说是因为讲解的需要——全局变量是不建议使用的),我们用一个函数访问它们是能被Python理解的:

bar = 42
def foo():
    print bar

在这里,我们在foo函数里使用了全局变量bar,然后它也如预想的能够正常运行:

>>> foo()
42

这样做很酷。通常,我们在使用了这个特性之后就想在所有的代码里用上它。如果像以下的例子中使用的话还是能够正常运行的:

bar = [42]
def foo():
    bar.append(0)
foo()
>>> print bar
[42, 0]

但是,如果我们把bar变一下呢:

>>> bar = 42
... def foo():
...     bar = 0
... foo()
... print bar
42

我们可以看到foo函数运行的好好的并且没有抛出异常,但是当我们打印bar的值的时候会发现它的值仍然是42。造成这种情况的原因就是 bar=0 这行代码,它没有改变全局变量bar的值,而是创建了一个名字也叫bar的局部变量并且它的值为0。这是个很难发现的bug,这会让没有真正理解Python作用域的新手非常痛苦。为了理解Python是如何处理局部变量和全局变量的,我们来看一种更少见的,但是可能会更让人困惑的错误,我们在打印bar的值后定义一个叫bar这个局部变量:

bar = 42
def foo():
    print bar
    bar = 0

这样写应该是不会出错的,不是吗?我们在打印了值之后定义了相同名称的变量,所以这应该是不会影响的(Python毕竟是一种解释型语言),真的是这样吗?

结果出错了:UnboundLocalError: local variable 'bar' referenced before assignment

Python的动态性和解释型的特性让我们相信当 print bar 这行被执行的时候,Python会在首先在局部作用域里寻找叫bar的变量然后再去寻找全局作用域里的。但实际上发生的是局部作用域不是完全动态的。当def 这个声明执行的时候,Python会静态地从这个函数的局部作用域里获取信息。当来到 bar=0 这行的时候(不是执行到这行代码,而是当Python解释器读到这行代码的时候),它会把’bar’这个变量加入到foo函数的局部变量列表里。当foo函数执行并且Python准备执行print bar这行的时候,它就会在局部的作用域里寻找这个变量,由于这个过程是静态的,Python知道这个变量还没有被赋值,这个变量没有值,所以抛出了异常。

解决方法一:

是使用global关键字。这是不言自明的。这会让Python知道bar是一个全局变量而不是局部变量。

>>> bar = 42
... def foo():
...     global bar
...     print bar
...     bar = 0
... 
... foo()
42
>>> bar
0
解决方法二

这也是更推荐使用的,就是不要使用全局变量。在我的大量Python开发工作中从来没有用到global这个关键字。能知道怎么用它就行了,但最终还是要尽量避免使用它。如果你想保存在代码里至始至终用到的值的时候,把它定义为一个类的属性。用这种方法的话就完全不需要用global了,当你要用这个值的时候,通过类的属性来访问就可以了:

>>> class Baz(object):
...     bar = 42
... 
... def foo():
...     print Baz.bar  # global
...     bar = 0  # local
...     Baz.bar = 8  # global
...     print bar
... 
... foo()
... print Baz.bar
42
0
8