У нас уже был пользовательский импортер (отказ от ответственности: я не писал этот код, я просто текущий сопровождающий), чей 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
, переопределяемыми методами, которые в моем переопределении оставят этот кеш в покое:
So:
@@ -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",)
Хм ? В настоящее время я импортирую тот же самый модуль. Что ж, ответ, вероятно, находится в документах по импорту
Если модуль не найден в кеше, выполняется поиск 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]
Это, однако, взорвалось по-новому - или, скорее, двумя способами:
ссылка на игру/пакет содержалась в bash
верхнем экземпляре пакета в sys.modules:
bash\
__init__.py
the_code_in_question_is_here.py
game\
...
потому что game
импортируется как bash.game
. Эта ссылка содержала ссылки на все game1, game2,...
подпакеты, поэтому они никогда не удалялись сборщиком мусора.
- ссылка на другой модуль (brec) удерживалась как
bash.brec
тем же экземпляром модуля bash
. Эта ссылка была импортирована как from .. import brec
в game\game1 без активации импорта для обновления SomeClass
. Однако в еще одном модуле импорт формы from ...brec import SomeClass
действительно инициировал импорт, и еще один экземпляр модуля brec оказался в sys .модули. Этот экземпляр имел необновленный SomeClass
и выдал ошибку AttributeError.
Оба были исправлены путем удаления этих ссылок вручную, поэтому gc собрал все модули (для 5 мегабайт оперативной памяти из 75), и from .. import brec
действительно вызвал импорт (это from ... import foo
против from ...foo import bar
требует вопроса).
Мораль этой истории в том, что это возможно, но:
- пакет и подпакеты должны ссылаться только друг на друга
- все ссылки на внешние модули/пакеты должны быть удалены из атрибутов пакета верхнего уровня.
- сама ссылка на пакет должна быть удалена из атрибута пакета верхнего уровня
Если это звучит сложно и чревато ошибками, так оно и есть — по крайней мере, теперь у меня гораздо более четкое представление о взаимозависимостях и их опасностях — пора заняться этим.
Этот пост был спонсирован отладчиком Pydev - я нашел модуль gc
очень полезным для понимания того, что происходит - советы из здесь. Конечно, было много переменных отладчика и других сложных вещей.
person
Mr_and_Mrs_D
schedule
29.04.2017
python -m py_compile script.py
? - person fedepad   schedule 27.01.2017AttributeError
илиArithmeticError
илиKeyError
и т. д. во время импорта. Простой импорт OTOH не гарантирует, что импортированные функции в любом случае не будут аварийно завершать работу во время выполнения. - person 9000   schedule 27.01.2017eval
предоставление одноразового словаря сохраняет пространство имен вашего интерпретатора, хорошая идея! OTOH это не мешает оцениваемому модулю выполнять произвольный ввод-вывод, по крайней мере, если вы не очень оборонительны (что сложно). Это зависит от того, сколько саддбоксинга вам нужно, например. импорт кода, загруженного из Интернета, по сравнению с проверкой работоспособности кода, которому вы больше всего доверяете. - person 9000   schedule 27.01.2017eval
? Это явно актуально - см. комментарии выше. На самом деле указанный путь компиляции, вероятно, является низшим способом проверки правильности, поскольку он пропустит упомянутые ошибки. - person Mr_and_Mrs_D   schedule 27.01.2017OTOH mere importing does not guarantee that imported functions will not crash at runtime anyway
- лол, конечно - я не ищу волшебного метода, который проверит, что моя программа не содержит ошибок - просто для эквивалента приведенному выше коду - эквивалентный код должен пройти iff выше проходит.vs doing a sanity check for code you mostly trust
- см. выше - мне нужен код, эквивалентный приведенному вышеtry: import except: print 'error'; continue
- person Mr_and_Mrs_D   schedule 27.01.2017