您的位置:首页 > 编程语言 > Python开发

python中常见的函数陷阱

2016-08-03 00:30 253 查看
本地变量是静态检测的

正如我们所知道的一样,Python定义的在一个函数中进行分配的变量名是默认为本地变量的,它们存在于函数的作用域并只在函数运行时存在。Python是静态检测Python的本地变量的,当编译def代码时,不是通过发现赋值语句在运行时进行检测的。这导致了在Python中入门者最为常见的陷阱之一。

一般来说,没有在函数中赋值的变量名会在整个模块文件中查找。

X = 99
def selector():
print(X)

>>>selector()
99


这里,函数中的X被解析为模块中的X。但是如果在引用之后增加了一个赋值语句,看看会发生什么。

def selector():
print(X)
X = 88

>>>selector()

UnboundLocalError: local variable 'X' referenced before assignment
你得到了一个未定义变量名的错误,但其原因是微妙的。在交互模式下输入或从一个模块文件导入时,Python读入并编译这段代码。在编译时,Python看到了对X的赋值语句,并且决定了X将会在函数中的任意地方都将是本地变量名。但是,当函数实际运行时,因为在print执行时赋值语句并没有发生,Python告诉你正在使用一个未定义的变量名。根据其变量名规则,本地变量X是在其被赋值前就被使用了。实际上,任何在函数体内的赋值将会使其成为一个本地变量名。Import、=、嵌套def、嵌套类等,都会受这种行为的影响。

产生这种问题的原因在于被赋值的变量名在函数内部室当作本地变量来对待的,而不是仅仅在赋值以后的语句中才被当做是本地变量。实际上,前一个例子是最含糊不清的:是希望打印一个全局变量X之后创建一个本地变量X,还是这真的是一个程序错误?因为Python会在函数中将X作为本地变量,它就是一个错误。如果你真的想要打印全局变量X,需要在一个global语句中声明这一点。

def selector():
global X
print(X)
X = 88

>>>selector()
99
记住,尽管这样,这一位置的赋值语句同样会改变全局变量X,而不是一个本地变量。在函数中,不可能同时使用同一个简单变量名的本地变量和全局变量。如果真的希望打印全局变量,并在之后设置一个有相同变量名的本地变量,导入上层的模块,并使用模块的属性标记来获得其全局变量。

X = 99
def selector():
import __main__
print(__main__.X)
X = 88
print(X)

selector()
99
88
点号运算(.X这部分)从命名空间对象中获取了变量的值。交互模式下的命名空间是一个名为__main__的命名空间,所以__main__.X得到了全局变量版本的X。

在Python最近的版本中,已经针对这种情况发布了更为专用的“unbound local”错误消息来改进这一问题;然而这个陷阱仍然普遍存在。

默认和可变对象

默认参数是在def语句运行时评估并保存的,而不是在这个函数调用时。从内部来讲,Python会将每一个默认参数保存成一个对象,附加在这个函数本身。

这也就是通常我们想要的:因为默认参数是在def时被评估的,如果必要的话,它能够从整个作用域内保存值,但是因为默认参数在调用之间都保存了一个对象,必须对修改可变的默认参数十分小心。例如,下面的函数使用了一个空列表作为默认参数,并在函数每次调用时都对它进行了改变。

def saver(x=[]):
x.append(1)
print(x)

>>>saver([2])
[2,1]
>>>saver()
[1]
>>>saver()
[1,1]
>>>saver()
[1,1,1]


有些人把这种行为当作一种特性。因为可变类型的默认参数在函数调用之前保存了它们的状态,从某种意义上讲它们能够充当C语言中的静态本地函数变量的角色。在一定程度上,它们工作起来就像全局变量,但是它们的变量名对于函数来说是本地变量,而且不会与程序中的其他变量名发生冲突。

尽管这样,对于大多数人来说,这看起来就像一个陷阱,特别是第一次遇到这样的情况的时候。在Python中有更好的办法在调用之间保存状态(例如,使用类)。

此外,可变类型默认参数记忆起来比较困难(理解起来也不容易)。它们的值取决于默认对象构建的时间。在上一个例子中,其中只有一个列表对象作为默认值,这个列表对象是在def语句执行时被创建的。不会每次函数调用时都得到一个新的列表,所以每次新的元素加入后,列表会变大,对于每次调用,它都没有重置为空列表。

如果这不是你想要的行为的话,在函数主体的开始对默认参数进行简单的拷贝,或者将默认参数值的表达式移至函数体内部。只要值是存在于代码中,而这部分代码在函数每次运行时都会执行的话,你就会每次都得到一个新的对象。

def saver(x=None):
if x is None:
x = []
x.append(1)
print(x)

>>>saver([2])
[2,1]
>>>saver()
[1]
>>>saver()
[1]


使用函数属性:

def saver():
saver.x.append(1)
print(saver.x)

>>>saver.x = []
>>>saver()
[1]
>>>saver()
[1,1]
>>>saver()
[1,1,1]


该函数的名称对于函数自身来讲是全局的,但是,它不需要声明,因为它在函数内部是不会直接修改的。这并不是总以完全的方式使用,但是,当这样编写代码的时候,一个对象到函数的附加总是更加明确(并且肯定更容易理解)。

没有return语句的函数
在Python函数中,return(以及yield)语句是可选的。当一个函数没有精确的返回值的时候,函数在控制权从函数主体脱离时,函数将会退出。从技术上讲,所有的函数都返回了一个值,如果没有提供return语句,函数将自动返回None对象:
def proc(x):
print(x)

>>>x = proc('testing 123...')
testing 123...
>>>print(x)
None
没有return语句的函数与Python对应于一些其他语言中所谓的“过程”是等效的。它们常被当做语句,并且None这个结果被忽略了,就像它们只是执行任务而不需要计算有用的结果一样。
了解这些内容是值得的,因为如果你想要尝试使用一个没有返回值的函数的结果时,Python不会告诉你。例如,将一个列表添加方法的结果赋值不会导致错误,但是得到的会是None,而不是改变后的列表。
>>>list = [1,2,3]
>>>list = list.append(4)
>>>print(list)
None
这样的函数执行任务也会有副作用,就是它们往往设计成语句来运行,而不是表达式。

以上内容来自Python学习手册第四部分第20章,内容有删减。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  python 函数