我们已经有一个custom importer(免责声明:我没有编写该代码,我只是当前的维护者)其load_module:
def load_module(self,fullname):
if fullname in sys.modules:
return sys.modules[fullname]
else: # set to avoid reimporting recursively
sys.modules[fullname] = imp.new_module(fullname)
if isinstance(fullname,unicode):
filename = fullname.replace(u'.',u'\\')
ext = u'.py'
initfile = u'__init__'
else:
filename = fullname.replace('.','\\')
ext = '.py'
initfile = '__init__'
try:
if os.path.exists(filename+ext):
with open(filename+ext,'U') as fp:
mod = imp.load_source(fullname,filename+ext,fp)
sys.modules[fullname] = mod
mod.__loader__ = self
else:
mod = sys.modules[fullname]
mod.__loader__ = self
mod.__file__ = os.path.join(os.getcwd(),filename)
mod.__path__ = [filename]
#init file
initfile = os.path.join(filename,initfile+ext)
if os.path.exists(initfile):
with open(initfile,'U') as fp:
code = fp.read()
exec compile(code, initfile, 'exec') in mod.__dict__
return mod
except Exception as e: # wrap in ImportError a la python2 - will keep
# the original traceback even if import errors nest
print 'fail', filename+ext
raise ImportError, u'caused by ' + repr(e), sys.exc_info()[2]
所以我想我可以用可覆盖的方法替换访问sys.modules 缓存的部分,这些方法将在我的覆盖中单独保留该缓存:
所以:
@@ -48,2 +55,2 @@ class UnicodeImporter(object):
- if fullname in sys.modules:
- return sys.modules[fullname]
+ if self._check_imported(fullname):
+ return self._get_imported(fullname)
@@ -51 +58 @@ class UnicodeImporter(object):
- sys.modules[fullname] = imp.new_module(fullname)
+ self._add_to_imported(fullname, imp.new_module(fullname))
@@ -64 +71 @@ class UnicodeImporter(object):
- sys.modules[fullname] = mod
+ self._add_to_imported(fullname, mod)
@@ -67 +74 @@ class UnicodeImporter(object):
- mod = sys.modules[fullname]
+ mod = self._get_imported(fullname)
并定义:
class FakeUnicodeImporter(UnicodeImporter):
_modules_to_discard = {}
def _check_imported(self, fullname):
return fullname in sys.modules or fullname in self._modules_to_discard
def _get_imported(self, fullname):
try:
return sys.modules[fullname]
except KeyError:
return self._modules_to_discard[fullname]
def _add_to_imported(self, fullname, mod):
self._modules_to_discard[fullname] = mod
@classmethod
def cleanup(cls):
cls._modules_to_discard.clear()
然后我在 sys.meta_path 中添加了导入器,一切顺利:
importer = sys.meta_path[0]
try:
if not hasattr(sys,'frozen'):
sys.meta_path = [fake_importer()]
perform_the_imports() # see question
finally:
fake_importer.cleanup()
sys.meta_path = [importer]
对吗?错了!
Traceback (most recent call last):
File "bash\bush.py", line 74, in __supportedGames
module = __import__('game',globals(),locals(),[modname],-1)
File "Wrye Bash Launcher.pyw", line 83, in load_module
exec compile(code, initfile, 'exec') in mod.__dict__
File "bash\game\game1\__init__.py", line 29, in <module>
from .constants import *
ImportError: caused by SystemError("Parent module 'bash.game.game1' not loaded, cannot perform relative import",)
嗯?我目前正在导入相同的模块。那么答案大概在import's docs
如果在缓存中找不到模块,则搜索 sys.meta_path(sys.meta_path 的规范可以在 PEP 302 中找到)。
这并不完全正确,但我 猜 是语句 from .constants import * 查找 sys.modules 以检查父模块是否存在,并且我认为没有办法绕过它(请注意,我们的自定义加载器正在使用内置的模块导入机制,mod.__loader__ = self 是事后设置的)。
所以我更新了我的 FakeImporter 以使用 sys.modules 缓存,然后清理它。
class FakeUnicodeImporter(UnicodeImporter):
_modules_to_discard = set()
def _check_imported(self, fullname):
return fullname in sys.modules or fullname in self._modules_to_discard
def _add_to_imported(self, fullname, mod):
super(FakeUnicodeImporter, self)._add_to_imported(fullname, mod)
self._modules_to_discard.add(fullname)
@classmethod
def cleanup(cls):
for m in cls._modules_to_discard: del sys.modules[m]
然而,这以一种新的方式发生了 - 或者更确切地说是两种方式:
-
对 game/ 包的引用保存在 sys.modules 中的 bash 顶级包实例中:
bash\
__init__.py
the_code_in_question_is_here.py
game\
...
因为game 被导入为bash.game。该引用包含对所有 game1, game2,... 子包的引用,因此这些子包永远不会被垃圾收集
- 对另一个模块 (brec) 的引用被同一个
bash 模块实例保存为 bash.brec。此引用在 game\game1 未触发导入中以 from .. import brec 的形式导入,以更新 SomeClass。然而,在另一个模块中,from ...brec import SomeClass 形式的导入 did 触发了导入,并且 brec 模块的 另一个 实例最终在系统模块。该实例有一个未更新的 SomeClass 并出现 AttributeError。
通过手动删除这些引用来修复两者 - 因此 gc 收集了所有模块(75 个中的 5 MB 内存)并且 from .. import brec 确实触发了导入(这 from ... import foo 与 from ...foo import bar 值得提问)。
这个故事的寓意是它是可能的,但是:
- 包和子包只能相互引用
- 应从顶级包属性中删除对外部模块/包的所有引用
- 包引用本身应该从顶级包属性中删除
如果这听起来很复杂且容易出错,那么 - 至少现在我对相互依赖关系及其危险有了更清晰的认识 - 是时候解决这个问题了。
这篇文章是由 Pydev 的调试器赞助的 - 我发现 gc 模块对于了解正在发生的事情非常有用 - 来自 here 的提示。当然有很多变量是调试器的和那些复杂的东西