常见的问题

很大程度上来讲,Python作为一门简洁一致的语言,会尽量避免一些让人感到惊讶的特性。然而,还是有一些情况会让新手感到迷惑不解。

这些情形中的一些是故意为之,但却可能会令人感到惊讶。一些则已被证明是语言的缺陷。通常,对于一系列难以捉摸的行为,乍一看很奇怪,但是一旦你明白这种惊讶背后的底层原因,你就会觉得合乎情理。

可变的默认参数

对于Python初学者而言,最为常见、看起来令人惊讶的就是Python对于函数定义中可变默认参数的处理。

你所写的代码

def append_to(element, to=[]):
    to.append(element)
    return to

你所期望发生的

my_list = append_to(12)
print my_list

my_other_list = append_to(42)
print my_other_list

每次调用函数时,如果第二个参数没有提供,那么应该创建一个新的列表,所以输出应该是:

[12]
[42]

事实上发生的

[12]
[12, 42]

一旦函数被定义后,新的列表就会被创建,后续的每次函数调用都会使用同一个列表。

当函数被定义时,Python的默认参数就会被求值,而不是发生在每次函数调用时(这和Ruby是一样的)。这就意味着,如果你使用了可变的默认参数且改变该参数,你 将会 并且已经无意识的修改了之后所有调用该函数时的参数对象。

如何处理这种情况

每次函数调用时,通过使用一个默认的参数值(None 通常是一个好的选择)来提示该调用未提供参数,这时再创建一个新的对象。

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

当问题就不再是问题

有时候你可以专门“利用”(注:按所预期的使用)这个行为来维护函数调用之间的状态。通常在编写缓存函数的时候会用到这个特性。

延迟绑定闭包

另外一个迷惑源自Python在闭包中绑定变量的方式(或在周围全局作用域)。

你所写的代码

def create_multipliers():
    return [lambda x : i * x for i in range(5)]

你所期望发生的

for multiplier in create_multipliers():
    print multiplier(2)

包含有5个函数的列表,每个函数拥有自己的封闭的 i 变量来与其参数进行乘运算,产生结果如下:

0
2
4
6
8

事实上发生的

8
8
8
8
8

5个函数都已创建,然而这些函数却都把 x 乘以4。

Python的闭包采用 延迟绑定 。这意味着,闭包中使用到的变量,它的值是在内部函数被调用时才会进行查找。

这里例子中,无论何时,当所返回函数中的 任何 一个进行调用时, i 的值只有在调用时刻才在周边作用域中进行查找。而此刻,循环操作早已结束且 i 最后的值已变为4。

对于这个问题,更糟糕的一点是对这个结果的广泛误解:人们以为这与Python中的 lambdas 有关。其实,使用 lambda 表达式创建的函数没有任何特殊,事实上,使用常用的 def 创建的函数同样存在这个问题。

def create_multipliers():
    multipliers = []

    for i in range(5):
        def multiplier(x):
            return i * x
        multipliers.append(multiplier)

    return multipliers

如何处理这种情况

最为常用的解决方案可能需要一点hack。多亏前面提及的关于函数参数默认值求值问题(参见 可变的默认参数 ),你可以创建一个闭包,然后利用默认的参数值立刻绑定其参数,就像下面这样:

def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]

另外一个方案,你可以使用函数 functools.partial:

from functools import partial
from operator import mul

def create_multipliers():
    return [partial(mul, i) for i in range(5)]

当问题就不再是问题

有时候,你是希望闭包可以按照这种延迟绑定的行为来执行的。在很多情形下,延迟绑定是非常有用的。当然,很不幸,通过循环来创建唯一函数成了一个反例的情形。

无处不在的字节码(.pyc)文件

默认情况下,当从一个文件执行Python代码的时候,Python解释器会自动在磁盘上产生该文件的字节码文件,例如: module.pyc

这些 .pyc 文件不应该提交到源码的版本库中。

从理论上讲,由于性能的原因,这种行为默认是开启的。因为如果没有这些字节码文件,每次源码文件被载入执行时,都需要重新生成字节码。

禁用字节码(.pyc)文件

幸运的是,产生字节码的过程极其快速,所以在开发代码的时候并不需要担心这些。

当然,这些字节码文件相当的烦人,所以可以通过下面的方法来摆脱它们:

$ export PYTHONDONTWRITEBYTECODE=1

一旦设置 $PYTHONDONTWRITEBYTECODE 环境变量,Python就不会再产生字节码文件,这样你的开发环境可以保持干净整洁。

我建议在你的 ~/.profile 文件中设置这个环境变量。

移除字节码(.pyc)文件

如果已经存在字节码文件,下述命令可以移除这些文件:

$ find . -type f -name "*.py[co]" -delete -or -type d -name "__pycache__" -delete

在项目的跟目录下执行这行命令,所有的 .pyc 文件瞬间就会消失的无影无踪。6不6?