如何使用现代 Python 重定向模块导入?

How can I redirect module imports with modern Python?

我正在维护一个 python 包,我在其中进行了一些重组。现在,我想支持仍然使用 from my_package.old_subpackage.foo import Foo 而不是新的 from my_package.new_subpackage.foo import Foo 的客户,而无需明确重新引入许多进行转发的文件。 (old_subpackage 仍然存在,但不再包含 foo.py。)

我知道有“加载器”和“查找器”,我的印象是我应该为我的目的实现一个 loader,但我只实现了一个查找器 目前为止:

RENAMED_PACKAGES = {
    'my_package.old_subpackage.foo': 'my_package.new_subpackage.foo',
}

# TODO: ideally, we would not just implement a "finder", but also a "loader"
# (using the importlib.util.module_for_loader decorator); this would enable us
# to get module contents that also pass identity checks
class RenamedFinder:

    @classmethod
    def find_spec(cls, fullname, path, target=None):
        renamed = RENAMED_PACKAGES.get(fullname)
        if renamed is not None:
            sys.stderr.write(
                f'WARNING: {fullname} was renamed to {renamed}; please adapt import accordingly!\n')
            return importlib.util.find_spec(renamed)
        return None

sys.meta_path.append(RenamedFinder())
然而,

https://docs.python.org/3.5/library/importlib.html#importlib.util.module_for_loader 和相关功能似乎已被弃用。我知道这不是我想要实现的非常 pythonic 的事情,但我很高兴得知它是可以实现的。

在旧模块的初始化文件中,从较新的模块导入它
旧 (package.oldpkg):

foo = __import__("Path to new module")

新(package.newpkg):

class foo:
  bar = "thing"

所以
package.oldpkg.foo.bar 与 package.newpkg.foo.bar

相同

希望对您有所帮助!

将所有旧包名称合并为my_package
旧包(old_package):

  • image_processing (class) 将被删除并替换为 better_image_processing
  • text_recognition (class) 将被删除并替换为 better_text_recognition
  • foo (变量) 会被移动到 better_text_recognition
  • still_there(class)不会动

新包:

  • super_image_processing
  • 更好_text_recognition

重定向器(my_package 的 class):

class old_package:
   image_processing = super_image_processing # Will be replaced
   text_recognition = better_text_recognition # Will be replaced

你的主要新模块(my_package):

#imports here
class super_image_processing:
  def its(gets,even,better):
    pass
class better_text_recognition:
  def now(better,than,ever):
    pass
class old_package:
   #Links
   image_processing = super_image_processing
   text_recognition = better_text_recognition
   still_there = __import__("path to unchanged module")

这允许您删除一些 文件 并保留其余的。如果你想重定向变量你会这样做:

class super_image_processing:
  def its(gets,even,better):
    pass
class better_text_recognition:
  def now(better,than,ever):
    pass
class old_package:
   #Links
   image_processing = super_image_processing
   text_recognition = better_text_recognition
   foo = text_recognition.foo
   still_there = __import__("path to unchanged module")

这行得通吗?

在导入包的 __init__.py 时,您可以将任何您想要的对象放入 sys.modules,您放入其中的值将通过 import 语句返回:

from . import new_package
from .new_package import module1, module2
import sys

sys.modules["my_lib.old_package"] = new_package
sys.modules["my_lib.old_package.module1"] = module1
sys.modules["my_lib.old_package.module2"] = module2

如果有人现在使用 import my_lib.old_packageimport my_lib.old_package.module1,他们将获得对 my_lib.new_package.module1 的引用。由于导入机器已经在 sys.modules 字典中找到了键,它甚至从未开始寻找旧文件。

如果您想避免立即导入所有子模块,可以通过在 sys.modules:

中放置一个带有 __getattr__ 的模块来模拟延迟加载
from types import ModuleType
import importlib
import sys

class LazyModule(ModuleType):
 def __init__(self, name, mod_name):
  super().__init__(name)
  self.__mod_name = name

 def __getattr__(self, attr):
  if "_lazy_module" not in self.__dict__:
    self._lazy_module = importlib.import(self.__mod_name, package="my_lib")
  return self._lazy_module.__getattr__(attr)

sys.modules["my_lib.old_package"] = LazyModule("my_lib.old_package", "my_lib.new_package")

我认为这就是您要找的:

RENAMED_PACKAGES = {
    'my_package.old_subpackage.foo': 'my_package.new_subpackage.foo',
}

class RenamedFinder:

    @classmethod
    def find_spec(cls, fullname, path, target=None):
        renamed = RENAMED_PACKAGES.get(fullname)
        if renamed is not None:
            sys.stderr.write(
                f'WARNING: {fullname} was renamed to {renamed}; please adapt import accordingly!\n')
            spec = importlib.util.find_spec(renamed)
            spec.loader = cls
            return spec
        return None

    @staticmethod
    def create_module(spec):
        return importlib.import_module(spec.name)

    @staticmethod
    def exec_module(module):
        pass

sys.meta_path.append(RenamedFinder())

不过,IMO 更可取,因为它更具可读性、更明确,并为您提供更多控制。当 my_package.new_subpackage.foo 开始偏离 my_package.old_subpackage.foo 而您仍然需要提供旧版本以实现向后兼容性时,它可能会变得很有用,尤其是在您的软件包的其他版本中。出于这个原因,您可能需要保留两者的代码。