Python的import系统是非常强大的,但是也非常复杂。直到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开关去运行一个包内的模块,它就会自动发生。 考虑一个简单的包,其布局如下(我在我自己的工程里专门沿着这几条线使用了这样的包布局——许多人讨厌在像这样的包目录里做嵌套测试,而喜欢平行的结构,但是我更喜欢使用显式的相对的导入方式来保证模块测试与包名称独立这样的能力。
长期以来,用这种启动方式唯一能让sys.path正确的方法是或者在test_foo.py中手动设置(很少有Python的新手,甚至许多老手都不知道怎么做)或者确保导入模块而不是直接执行它:
当我正在使用一个嵌入式测试用例作为例子时,当你为了确保sys.path正确初始化了而没有在父目录使用-m开关去在包中直接执行一个脚本时,类似的问题随时都会发生(例如1.4版本之前的Django工程布局会在当从包内运行manage.py时产生问题,它会将包目录放入sys.path以致导致这个双重导入问题——1.4版本之后的布局通过把manage.py移到包目录外面而解决了这一问题)。 事实是大多数从命令行调用Python代码在当代码位于一个包内时都会崩溃,而两个可以工作的方式又对当前工作目录非常敏感,这对于新手来说非常困惑。我个人相信这是导致Python包复杂并且很难被正确使用这一观点的关键因素。 这个问题甚至不限于命令行——如果test_foo.py在IDLE中打开并且你试图通过F5运行它时,或者你试图在一个图像化的文件浏览器中通过点击它来运行时,它就会像通过命令行直接运行一样失败。 在sys.path中不要写包目录这一规则的存在有一个原因,即解释器当确定sys.path[0]是所有错误的根源时它自己也不会参照这一条规则。 然而,即使在未来版本的Python中在这个部分有许多改善(参见PEP 395),这个陷阱也会在所有当前版本中存在。 执行主模块两次 这是上述双重引用问题的一个变种,它不需要任何错误的sys.path条目。 对于当主模块也被作为普通模块导入的情形来说非常特别,实际上它会产生同一个模块的两个不同名称的实例。 正如任何双重导入问题,如果存储在__main__中的状态对于程序正确运行十分重要,或者在主模块中有一些顶级代码执行了不止一次会产生未知的副作用,之后,这个复制品也会产生复杂的意想不到的错误。 这仅仅是为什么在更加复杂的应用中主模块需要保持代码最少的一个原因——通常将大多数的功能移到在单独的模块里的一个函数或者一个对象中并在主模块中导入该模块会更加鲁棒。那样,不经意得执行主模块两次将变得没有害处。保证主模块精简也可以避免伴随着对象序列化以及多线程包的一些潜在的问题。 命名覆盖陷阱 另一个常见的陷阱,特别对于初学者来说,是使用一个本地模块名导致覆盖了程序所依赖的标准库的或是第三方的包或者模块。一个特别意想不到的碰到这个陷阱的情况是对一个脚本使用这样的名字,因为这会结合之前“执行主模块两次”陷阱导致问题。例如,如果尝试学习更多关于Python的socket模块,你可能倾向于命名你的实验脚本为socket.py。事实证明这是一个坏主意,因为使用这样的名字意味着Python解释器可以不再去标准库中寻找真正的socket模块,因为当前目录里的这个socket模块挡住了去路:
紧跟着之前小节的例子之后,假设我们决定通过重命名文件来修复我们错误的脚本名。在Python 2中,我们会发现这仍然不起作用:
子模块被加入包命名空间的陷阱 许多人已经体验过了在仅仅导入了子模块所在的包而去使用该子模块时存在的问题了:
更多的奇怪的陷阱 上面提到的都是一些平常的陷阱,但是还存在其他陷阱,尤其是如果你开始着手于扩展或者重写默认import系统的工作时。 最后我希望对这些增加一些细节描述:
英文原文:http://python-notes./en/latest/python_concepts/import_traps.html
|
|
来自: River_LaLaLa > 《Python》