如果从不同路径导入,模块会重新导入吗
在我正在开发的一个大型应用中,有好几个人以不同的方式导入相同的模块,比如有的人用
import x
而有的人用
from y import x
这样做的后果是,x这个模块被导入了两次,这可能会引入一些非常微妙的错误,尤其是当有人依赖全局属性的时候。
举个例子,假设我有一个包叫做mypackage,里面有三个文件:mymodule.py、main.py和init.py。
mymodule.py的内容是:
l = []
class A(object): pass
main.py的内容是:
def add(x):
from mypackage import mymodule
mymodule.l.append(x)
print "updated list",mymodule.l
def get():
import mymodule
return mymodule.l
add(1)
print "lets check",get()
add(1)
print "lets check again",get()
它打印的结果是:
updated list [1]
lets check []
updated list [1, 1]
lets check again []
因为现在在两个不同的模块中有两个列表,类似地,类A也是不同的。对我来说,这看起来是个严重的问题,因为类本身会被当作不同的东西来处理。
例如,下面的代码打印出的是False:
def create():
from mypackage import mymodule
return mymodule.A()
def check(a):
import mymodule
return isinstance(a, mymodule.A)
print check(create())
问题:
有没有什么办法可以避免这种情况?除了强制要求模块只能用一种方式导入。难道这不能通过Python的导入机制来处理吗?我在Django的代码和其他地方也见过与此相关的几个错误。
2 个回答
每个模块的命名空间只会被导入一次。问题在于,你导入它们的方式不同。第一次你是从全局包导入,而第二次你是做了一个本地的、非打包的import
。Python把这些模块视为不同的。第一次导入在内部被缓存为mypackage.mymodule
,而第二次则只被视为mymodule
。
解决这个问题的方法是始终使用绝对导入。也就是说,总是从顶层包开始,给你的模块提供绝对导入路径:
def add(x):
from mypackage import mymodule
mymodule.l.append(x)
print "updated list",mymodule.l
def get():
from mypackage import mymodule
return mymodule.l
记住,你的入口点(你运行的文件,main.py
)也应该在包外。当你想让入口点代码在包内时,通常会运行一个小脚本。比如:
runme.py
,在包外:
from mypackage.main import main
main()
然后在main.py
中添加:
def main():
# your code
我觉得Jp Calderone写的这篇文档是关于如何(不)构建你的Python项目的一个很好的建议。按照这个做法,你就不会遇到问题。注意bin
文件夹——它是在包外的。我会在这里复述整个内容:
Python项目的文件系统结构
应该做的:
- 给目录起个和你的项目相关的名字。例如,如果你的项目叫做"Twisted",那么顶层目录可以命名为
Twisted
。发布时,应该加上版本号后缀:Twisted-2.5
。- 创建一个
Twisted/bin
目录,把你的可执行文件放在那里,如果有的话。即使它们是Python源文件,也不要给它们加.py
扩展名。里面不要放任何代码,除了导入和调用在项目其他地方定义的主函数。- 如果你的项目可以用一个Python源文件表示,那么把它放到这个目录里,并给它起个和项目相关的名字。例如,
Twisted/twisted.py
。如果需要多个源文件,就创建一个包(Twisted/twisted/
,并在里面放一个空的Twisted/twisted/__init__.py
),把源文件放进去。例如,Twisted/twisted/internet.py
。- 把你的单元测试放在包的子包里(注意——这意味着上面提到的单个Python源文件选项是个技巧——你总是需要至少一个其他文件来进行单元测试)。例如,
Twisted/twisted/test/
。当然,要把它做成一个包,里面有Twisted/twisted/test/__init__.py
。把测试放在像Twisted/twisted/test/test_internet.py
这样的文件里。- 如果你愿意,可以添加
Twisted/README
和Twisted/setup.py
来解释和安装你的软件。不应该做的:
- 不要把源代码放在叫
src
或lib
的目录里。这会让运行变得困难。- 不要把测试放在Python包外面。这会让测试很难针对已安装的版本运行。
- 不要创建一个只有
__init__.py
的包,然后把所有代码都放在__init__.py
里。直接做一个模块会更简单。- 不要试图想出一些神奇的技巧,让Python能够导入你的模块或包,而不让用户把包含它的目录添加到他们的导入路径中(通过
PYTHONPATH
或其他机制)。你无法正确处理所有情况,用户会因为你的软件在他们的环境中无法正常工作而生气。
我只能在你实际运行的是main.py这个文件时复现这个问题。在这种情况下,你会在系统路径中得到main.py的当前目录。不过,显然你还有一个系统路径设置,这样就可以导入mypackage。
在这种情况下,Python不会意识到mymodule和mypackage.mymodule是同一个模块,因此你会看到这种效果。这个变化可以用下面的代码来说明:
def add(x):
from mypackage import mymodule
print "mypackage.mymodule path", mymodule
mymodule.l.append(x)
print "updated list",mymodule.l
def get():
import mymodule
print "mymodule path", mymodule
return mymodule.l
add(1)
print "lets check",get()
add(1)
print "lets check again",get()
$ export PYTHONPATH=.
$ python mypackage/main.py
mypackage.mymodule path <module 'mypackage.mymodule' from '/tmp/mypackage/mymodule.pyc'>
mymodule path <module 'mymodule' from '/tmp/mypackage/mymodule.pyc'>
但是在当前目录中再添加一个mainfile:
realmain.py:
from mypackage import main
结果就会不同:
mypackage.mymodule path <module 'mypackage.mymodule' from '/tmp/mypackage/mymodule.pyc'>
mymodule path <module 'mypackage.mymodule' from '/tmp/mypackage/mymodule.pyc'>
所以我怀疑你的主Python文件是在包里面。在这种情况下,解决办法就是不要这样做。:-)