基于目录名使用importlib加载同名类:后果是啥?

Posted

技术标签:

【中文标题】基于目录名使用importlib加载同名类:后果是啥?【英文标题】:Loading same-named classes with importlib based on directory name: What are the consequences?基于目录名使用importlib加载同名类:后果是什么? 【发布时间】:2021-05-29 19:57:14 【问题描述】:

当然,“除了明显的后果之外没有任何后果”是对我关于后果的问题的有效回答。

我帮助维护一个代码库,一些建议的代码结构大致如下:

# main.sh
export PYTHONPATH=$PYTHONPATH:$(pwd)
python main.py "brazil"
python main.py "france"
# main.py
from importlib.util import find_spec, module_from_spec
import sys

class BaseProcessor(object):
    # abstract base class
    pass

def run_for(country):
    spec = find_spec(country + ".processor")
    module = module_from_spec(spec)
    spec.loader.exec_module(module)
    processor = module.CountryProcessor()
    processor.do_something()

if __name__ == "__main__":
    run_for(sys.argv[1])
# brazil/processor.py
from main import BaseProcessor
class CountryProcessor(BaseProcessor):
    def do_something(self):
        print("This is the Brazil CountryProcessor")

# france/processor.py
from main import BaseProcessor
class CountryProcessor(BaseProcessor):
    def do_something(self):
        print("This is the France CountryProcessor")

run_for() 中的代码使用 importlib 根据传入的字符串 country 查找名为 processor 的模块。

输出如设计:

> ./main.sh 
This is the Brazil CountryProcessor
This is the France CountryProcessor

我们有一些类似的代码(但使用sys.path.insert())已经运行了几个月;我不知道有什么情况是同名的类引起了问题。

如果有的话,这种设计可能会产生什么意想不到的后果?

【问题讨论】:

您的问题到底是什么?您显示一些代码并声明它按预期工作。那么不清楚的部分是什么? 好吧,一方面,您正在使用单独的进程进行迭代,因此类相互混淆的风险相当低。如果您同时运行多个国家/地区,我会考虑在一个函数中加载,然后将其分配给命名空间而不是全局范围。类似someCountry.processor = module.CountryProcessor() a_guest:有什么后果?从长远来看,运行的代码通常会产生意想不到且可能不受欢迎的后果。我的问题是,这些后果是什么(如果有的话)? 虽然想起来了,但我忘记了软件工程堆栈交换。这是相当特定于 Python 的,所以我不知道它是否更适合那里或这里。 【参考方案1】:

在这里,我回答了我自己的问题,并为以下潜在的意外后果提出了理由。我渴望听到关于现实世界影响的其他意见。可能只是我在床底下看到了怪物。

重要的是要规定,这是我们预计会出现一段时间的代码,整个组件当然比我的玩具示例复杂很多倍。

    这种方法在以后会产生调试困难。
常规(例如可视)调试器将显示加载的类。如果所有类的名称都相同,我们就否认了调试帮助。 因此,如果在导入过程中出现了一些错误,将更难捕获。我们实质上是在欺骗调试器。 即使是基于日志的调试策略也容易出错。现在我们必须在每个类中维护不同的日志记录语句。复制/粘贴有忘记更改调试的风险。
    直觉上,它似乎很容易出错
这里有许多关于如何缓解来自两个不同库的意外名称冲突的 Q/A,通常是在导入时给它们不同的名称。 我们为什么要面对一个我们会努力避免的情况(如果我们不小心加载了其中两个)? 如果一个天真的编辑器试图在运行我们的 importlib 魔法之后的方法中静态导入,这将导致问题。我确实不时在我们的代码中看到这样的静态导入。 但我觉得还有一个更清晰的错误案例,我还无法表达。
    它通常只会增加复杂性,从而增加维护成本。
认知负荷很重要。 “简单胜于复杂”(Tim Peters,“Python 之禅”)。 与传统的进口和工厂相比,这种进口机制增加了复杂性,但没有太多的补偿收益。 (我在我的玩具示例中省略了它,但导入代码实际上分布在 3-4 个函数中。) 我想,如果我们有 40 个国家/地区,假设的收益是不必进行 40 次静态导入,但有更直观的方法来解决这个问题。 此代码不支持在同一个 Python 进程中加载​​多个国家。估计现在不会发生。但谁能说它不能走下去呢? 好的软件设计不会试图预测未来,而是尽可能保持灵活性。

在现实生活中,我认为这种方法反映了本可以部署到更持久解决方案的编程工作。现在,机会成本可能无关紧要 工作完成了。 (就上下文而言,建议使用此代码替换一些确实使用工厂和静态导入的代码,因此 IMO 的最终效果是引入反模式 一种工作机制。)


建议的替代方案

那么我该如何处理呢?如果导入的数量适中(例如,最多 40 或 50 ),我将使用简单的工厂类(甚至只是函数)对所有子类进行静态导入。 工厂可以维护一个查找表,或者(更好)它可以推断类名。将蛇案例转换为骆驼案例几乎是微不足道的。

如果数字更大,我仍然认为没有理由不加载所有类。只需遍历子目录列表一次,将它们转换为驼峰大小写,然后简单地导入它们 将类名转换为驼峰式。

Python 中额外导入的内存占用是如此微不足道,以至于我认为通常 Python 样式中的多余导入几乎没有缺点。如果由于某种原因 我们想要更明智地导入,我们可以将选择性导入保留在工厂类中,但仍然区分不同类的名称。又是一次 只要我们遵循模式,构造名称几乎是微不足道的。

【讨论】:

以上是关于基于目录名使用importlib加载同名类:后果是啥?的主要内容,如果未能解决你的问题,请参考以下文章

python importlib,如何使动态加载文件的缓存无效

编程艺术python importlib 动态调用 py 脚本方法

使用 `importlib` 从 Python 模块中仅导入特定类

Python importlib 动态加载模块

基于配置文件加载插件

Django模块importlib,requests