分享

Python Import系统给粗心人设下的“陷阱”

 River_LaLaLa 2016-08-22

Pythonimport系统是非常强大的,但是也非常复杂。直到Python 3.3版本的发布,都没有关于之前预计的import语义的全面的解释,甚至跟着3.3版本的发布,sys.path如何初始化的细节也仍然需要搞清楚。

即使3.3版本清除了许多东西,它仍旧需要搞定许多后向兼容性问题,这些问题可能导致一些奇怪的行为。并且,为了搞清一些第三方框架的运行机制,我们也需要充分了解3.3版本。

此外,即使不使用任何导入系统中奇异的特性,在邮件列表或者像Stack Overflow一样的Q&A网站中也经常出现相当多的常见的错误。

这篇短文的内容仅仅理论上向前包含至Python 2.6版本。大多数内容也适合于早期版本,但我不会对2.6以前的版本细节给出任何解释。

丢失的__init__.py陷阱

这个陷阱适用于2.x版本,也包括3.2及3.2之前的3.x版本。

Python 3.3之前,文件系统目录,以及zipfile中的目录,必须包含一个__init__.py文件以使它被识别为Python的包目录。即使当包被导入时没有初始化代码要运行,解释器仍然需要一个空的__init__.py文件以便在那个目录下能够找到任何模块或者子包。

这一情况在Python 3.3中改变了:现在任何一个在sys.path中的目录,如果和要查找的包名称一致,那么它将被视为该包的可以起作用的模块或子包

__init__.py的陷阱

这是一个在Python 3.3中增加的全新的陷阱,是由于修改之前的陷阱而带来的:如果一个sys.path所导入的包的一个子目录下也包含一个__init__.py文件,则Python解释器会创建一个仅仅包含来自于该目录下的单目录包,而不是像之前一节描述的一样,去寻找所有具有相同名称的子目录。

即使在sys.path中存在其他的不包括__init__.py文件子目录但是和要找的包名称相同,问题也同样会发生。

这一复杂情况是由于后向兼容性限制而强加于我们的——如果没有这个问题,当Python 3.3让用户可选是否在包中需要创建__init__.py文件的时候,一些现存的代码可能会崩溃。

然而,这一点也是很有用的,因为它使得显式地声明一个包已经完成,不再接受额外贡献代码变得可能。所有的标准库目前都是这样工作的,虽然一些包可能会开放它们的命名空间来在未来版本中接受第三方的贡献代码(特别的,encodings包将确定在Python 3.4时开放)。

双重引用陷阱

紧接着的这个陷阱存在于目前所有的Python版本中,包括Python 3.3,并且可以用下面一句话总结:永远不要直接向Python路径中添加一个包目录,或者包内的任何目录

这样做的原因是在那个目录下的每一个模块现在都潜在地有两个不同的可以访问的名字:作为顶级模块(由于目录在sys.path中)以及作为包的子模块(如果高一级的包含包本身的目录也在sys.path中)。

举个例子,Django(直到并包括1.3版本)在为特定站点创建应用时的做法是错误的——这个应用最后可以在模块命名空间中被作为app以及site.app来接入,并且事实上存在两份不同的模块的副本。如果有任何有意义的可变的模块级的状态,上述情况会导致困惑,所以这一行为从1.4版本中默认的文件夹结构中移除了(特定站点的应用将一直需要像Django版本说明中叙述的一样,完全匹配站点名称才可以)。

不幸的是,这仍然是十分容易违反的规则,因为如果你试图从命令行通过文件名而不是使用-m开关去运行一个包内的模块,它就会自动发生。

考虑一个简单的包,其布局如下(我在我自己的工程里专门沿着这几条线使用了这样的包布局——许多人讨厌在像这样的包目录里做嵌套测试,而喜欢平行的结构,但是我更喜欢使用显式的相对的导入方式来保证模块测试与包名称独立这样的能力。


这个布局令人惊讶的是所有的下列调用test_foo.py的方法可能都不起作用,因为导入机制坏掉了(或者说不能通过像这样import example.foo或者from example import foo找到example,或者不能在一个非包的或者顶级包以上的包中像from .. import foo一样的相对引用,或者如果一些其他的子模块碰巧覆盖了测试所用的顶级包的名称,例如一个负责序列化的example.json模块或者一个测试机模块example.tests.unittest,一些更加难以理解的错误可能会发生):


没错,如果你尝试那个长长的包含所有调用方法的列表,它很可能损坏了
并且如果你既对Python导入系统工作的方式不够熟悉,又对它如何初始化不够熟悉,那么错误消息就没有任何意义。(注意到如果工程仅仅显式地使用了包内引用的相对导入,则上面的最后两条命令可能在Python 3.3以及未来版本中可以工作。任何引用一个顶级包并进行绝对导入的做法将仍然会出错。

长期以来,用这种启动方式唯一能让sys.path正确的方法是或者在test_foo.py中手动设置(很少有Python的新手,甚至许多老手都不知道怎么做)或者确保导入模块而不是直接执行它:


然而,从Python 2.6之后,下面命令仍然可以正常工作:


这最后一种方式就是我在用Python编程时喜欢使用的shell命令——离开我的工作目录,设置到工程目录,然后使用-m开关来执行相关的子模块,例如测试或者命令行工具。如果我需要在不同的目录下工作,好的,这就是为什么我喜欢开着多个shell会话。

当我正在使用一个嵌入式测试用例作为例子时,当你为了确保sys.path正确初始化了而没有在父目录使用-m开关去在包中直接执行一个脚本时,类似的问题随时都会发生(例如1.4版本之前的Django工程布局会在当从包内运行manage.py时产生问题,它会将包目录放入sys.path以致导致这个双重导入问题——1.4版本之后的布局通过把manage.py移到包目录外面而解决了这一问题)。

事实是大多数从命令行调用Python代码在当代码位于一个包内时都会崩溃,而两个可以工作的方式又对当前工作目录非常敏感,这对于新手来说非常困惑。我个人相信这是导致Python包复杂并且很难被正确使用这一观点的关键因素。

这个问题甚至不限于命令行——如果test_foo.pyIDLE中打开并且你试图通过F5运行它时,或者你试图在一个图像化的文件浏览器中通过点击它来运行时,它就会像通过命令行直接运行一样失败。

sys.path中不要写包目录这一规则的存在有一个原因,即解释器当确定sys.path[0]是所有错误的根源时它自己也不会参照这一条规则。

然而,即使在未来版本的Python中在这个部分有许多改善(参见PEP 395),这个陷阱也会在所有当前版本中存在。

执行主模块两次

这是上述双重引用问题的一个变种,它不需要任何错误的sys.path条目。

对于当主模块被作为普通模块导入的情形来说非常特别,实际上它会产生同一个模块的两个不同名称的实例。

正如任何双重导入问题,如果存储在__main__中的状态对于程序正确运行十分重要,或者在主模块中有一些顶级代码执行了不止一次会产生未知的副作用,之后,这个复制品也会产生复杂的意想不到的错误。

这仅仅是为什么在更加复杂的应用中主模块需要保持代码最少的一个原因——通常将大多数的功能移到在单独的模块里的一个函数或者一个对象中并在主模块中导入该模块会更加鲁棒。那样,不经意得执行主模块两次将变得没有害处。保证主模块精简也可以避免伴随着对象序列化以及多线程包的一些潜在的问题。

命名覆盖陷阱

另一个常见的陷阱,特别对于初学者来说,是使用一个本地模块名导致覆盖了程序所依赖的标准库的或是第三方的包或者模块。一个特别意想不到的碰到这个陷阱的情况是对一个脚本使用这样的名字,因为这会结合之前执行主模块两次陷阱导致问题。例如,如果尝试学习更多关于Pythonsocket模块,你可能倾向于命名你的实验脚本为socket.py。事实证明这是一个坏主意,因为使用这样的名字意味着Python解释器可以不再去标准库中寻找真正的socket模块,因为当前目录里的这个socket模块挡住了去路:


旧的字节码文件陷阱

紧跟着之前小节的例子之后,假设我们决定通过重命名文件来修复我们错误的脚本名。在Python 2中,我们会发现这仍然不起作用:


很明显,一些奇怪的事情发生了,因为我们看到在错误追踪中显示一个注释行出了问题。事实上,由我们之前错误的导入动作而缓存的字节码文件仍然存在,并且导致了这一问题,但是当Python试图在错误追踪中显示出错的源代码行时,它转而去标准库模块中寻找源代码行。删除旧的字节码文件可以让它正确运行:


这一特殊的陷阱在Python 3.2以及之后的版本已经彻底去除了。在这些版本中,解释器可以区别独立的字节码文件(例如上文中的socket.pyc)和缓存的字节码文件(存储在自动生成的__pycache__目录下)。如果相应的源文件不存在,后者会被解释器忽略掉,所以上述的为源文件重命名如期工作了:


然而,需要注意的是,如果Python 2留下了一个独立的字节码文件,混用Python 2Python 3会导致问题:


如果你不是某个Python实现的核心开发人员,导入旧的字节码的问题很可能当重命名Python源文件时发生。而对于Python实现的开发人员,它则可能在我们使用负责首先产生字节码的编译器部件的任何时刻发生——这就是为什么CPythonMakefile中包含一个make pycremoval目标。

子模块被加入包命名空间的陷阱

许多人已经体验过了在仅仅导入了子模块所在的包而去使用该子模块时存在的问题了:


然而很少被人知的一件事是,当一个子模块在无论任何地方加载时,它会被自动地添加到包的全局命名空间中:


在一个__init__.py文件中导入或者定义一个同当前包的子模块拥有相同名字的值时,结果可能会更让你惊讶。如果子模块在导入或者定义了相同名字的值之后的任意位置被任何模块加载了,它将会在__init__.py的全局命名空间中覆盖已经导入的或者定义的名字。

更多的奇怪的陷阱

上面提到的都是一些平常的陷阱,但是还存在其他陷阱,尤其是如果你开始着手于扩展或者重写默认import系统的工作时。

最后我希望对这些增加一些细节描述:

  •  __import__的怪异的签名

  • 模块全局变量(__import__, __path__, __package__)的影响

  • 3.3版本之前线程的问题

  • 3.3版本之前默认的机器缺少对PEP 302的支持

  • 3.3版本之前命名空间包中的非合作的包的部分

  • sys.path[0]初始化变量

  • 关于pickle, multiprocessing和主模块的问题的更多内容(参见PEP 395

  • __main__不总是一个顶级包(多亏了-m

  • 事实上,在导入过程中,模块是允许在sys.modules中替换他们自己的

  • __file__可能不指代一个实际文件系统的位置

  • 自从3.2之后,你不能仅仅添加c或者o来获得缓存的字节码文件


英文原文:http://python-notes./en/latest/python_concepts/import_traps.html
译者:LuCima


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多