通常,Python 旨在成为一门简洁一致的语言,避免发生意外。然而,有些情况可能会给新手们造成困惑。

在这些情况中,有一些虽是有意为之,但还是有潜在风险。还有一些则可以说是语言设计缺陷了。总之,下面列出的这些情况都是些乍一看很不好理解的行为,不过一旦您了解了这些奇怪行为背后的机理,也就基本上能理解了。

可变默认参数

似乎每个 Python 新手都会感到惊讶的一点是 Python 在函数定义中对待可变默认参数的方法。

您所写的

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

您可能期待的结果

1
2
3
4
5
my_list = append_to(12)
print my_list

my_other_list = append_to(42)
print my_other_list

函数每次被调用时,如果不提供第二个参数,就创建一个新的列表。所以结果就应该是:

1
2
[12]
[42]

实际上的结果

1
2
[12]
[12, 42]

一旦 完成了函数定义,一个新的列表就创建出来了,而且在随后的每一次函数调用中被使用的都是这个列表。

一旦 完成了函数定义,Python 的默认参数就被赋值了,而且在随后的每一次函数调用中都不会再被默认值重复赋值(就像是在,嗯,Ruby 里那样)。这就意味着如果您使用了一个可变默认参数,并且改变了它,您也会且 将会 在未来的所有函数调用中改变这同一个参数对象。

您实际上应该做的

使用一个默认值来表示我们并不想给这个参数赋值,从而每次在函数被调用时我们都创建一个新的对象。(None 作为默认值通常是个好选择)。

1
2
3
4
5
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to

可别忘了,您所传递的第二个参数仍应该是个 列表 对象。

利用好『缺陷』

有时你可以专门 利用(或者说特地使用)这种行为来维护函数调用间的状态。这通常用于编写缓存函数。

延迟绑定闭包

另一个常见的困惑是 Python 在闭包(或在周围全局作用域)中绑定变量的方式。

当你写下

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

你期望发生

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

一个包含五个函数的列表,每个函数有它们自己的封闭变量 i 乘以它们的参数,得到:

1
2
3
4
5
0
2
4
6
8

而事实是:

1
2
3
4
5
8
8
8
8
8

五个函数被创建了,它们全都用 4 乘以 x

Python 的闭包是 延迟绑定的 。 这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。

这里,不论 任何 返回的函数是如何被调用的, i 取的是调用时周围作用域里的值。 当循环完成时, i 的值最终变成了 4。

关于这个陷阱有一个普遍严重的误解,它被认为只针对 Python 的 闭包 lambda 定义方式。 事实上,由 lambda 表达式创建的函数并没什么特别,同样的问题也出现在使用普通的 def 上:

1
2
3
4
5
6
7
8
9
def create_multipliers():
multipliers = []

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

return multipliers

以上正确的做法应该是:

最简单通用的解决方案可以说是有点取巧(hack)。由于 Python 拥有在前文提到的『为函数默认参数赋值』的行为(参见 可变默认参数 ),你可以创建一个立即绑定参数的闭包,像下面这样:

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

或者,使用 functools.partial 函数:

1
2
3
4
5
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 文件不应该被纳入源代码仓库。

理论上,出于性能原因,此行为默认为开启。 没有这些字节码文件, Python 会在每次加载文件时重新生成字节码文件。

禁用字节码(.pyc)文件

幸运的是,生成字节码的过程非常快,在开发代码时不需要担心。

那些文件很讨厌,所以让我们摆脱他们吧!

1
$ export PYTHONDONTWRITEBYTECODE=1

使用 $PYTHONDONTWRITEBYTECODE 环境变量来命令 Python 不将这些文件写入磁盘, 开发环境将会保持整洁和干净。

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

删除字节码(.pyc)文件

以下是删除所有已存在字节码文件的好方法:

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

从项目根目录运行,所有 .pyc 文件会瞬间消失。

版本控制忽略

如果由于性能原因仍然需要 .pyc 文件,你可以随时将它们添加到版本控制存储库的忽略文件中。 流行的版本控制系统能够使用文件中定义的通配符来应用特殊规则。

一份忽略文件将确保匹配的文件未被检入存储库。 Git 使用 .gitignore,而 Mercurial 使用 .hgignore

忽略文件里至少应该具备以下内容:

1
2
3
syntax:glob   # This line is not needed for .gitignore files.
*.py[cod] # Will match .pyc, .pyo and .pyd files.
__pycache__/ # Exclude the whole folder

可按需添加更多文件和目录。下次提交到存储库时,这些文件将不被包括。

参考资料