如果从不同路径导入,模块会重新导入吗

9 投票
2 回答
1936 浏览
提问于 2025-04-15 14:30

在我正在开发的一个大型应用中,有好几个人以不同的方式导入相同的模块,比如有的人用

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 个回答

5

每个模块的命名空间只会被导入一次。问题在于,你导入它们的方式不同。第一次你是从全局包导入,而第二次你是做了一个本地的、非打包的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/READMETwisted/setup.py来解释和安装你的软件。

不应该做的:

  • 不要把源代码放在叫srclib的目录里。这会让运行变得困难。
  • 不要把测试放在Python包外面。这会让测试很难针对已安装的版本运行。
  • 不要创建一个只有__init__.py的包,然后把所有代码都放在__init__.py里。直接做一个模块会更简单。
  • 不要试图想出一些神奇的技巧,让Python能够导入你的模块或包,而不让用户把包含它的目录添加到他们的导入路径中(通过PYTHONPATH或其他机制)。你无法正确处理所有情况,用户会因为你的软件在他们的环境中无法正常工作而生气。
3

我只能在你实际运行的是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文件是在包里面。在这种情况下,解决办法就是不要这样做。:-)

撰写回答