如何在包中只导入没有 exec __init__.py 的子模块

Posted

技术标签:

【中文标题】如何在包中只导入没有 exec __init__.py 的子模块【英文标题】:How to only import sub module without exec __init__.py in the package 【发布时间】:2014-02-13 10:47:36 【问题描述】:

从包中导入子模块时,包文件夹中的 __init__.py 文件将首先执行,如何禁用此功能。有时我只需要一个包中的一个功能,导入整个包有点重。

例如,pandas.io.clipboard 模块不依赖于 pandas 中的任何其他函数。

from pandas.io.clipboard import clipboard_get 会导入函数,也会导入所有 pandas 常用模块。是否有一些方法只导入剪贴板模块,因为它是我自己的应用程序文件夹中的一个模块。

【问题讨论】:

一般不回答这个问题,但您可以使用 pyperclip 代替 pandas 剪贴板模块。 【参考方案1】:

没有,设计。如果您想在导入子模块时避免太多开销,您只需使用空的__init__.pys 来定义包。这样,导入包的开销几乎为零。

如果pandas 没有 这样做,那么您有没有 方法来导入pandas.io.clipboard 而无需先导入pandasio。您可以做的,但是它是一个巨大的hack并且它是等效的,是将clipboard模块作为普通模块导入作为一个子模块。您只需找到安装pandas 的位置(例如/usr/lib/pythonX.Y/dist-packages/)并将父包的路径插入sys.path(在您的情况下为/usr/lib/pythonX.Y/dist-packages/pandas/io)。然后您可以通过以下方式导入clipboard 包:

import clipboard

但请注意:

import clipboard
from pandas.io import clipboard as clipboard2
print(clipboard == clipboard2)

将打印False。事实上,这样做会破坏很多代码,因为您从根本上破坏了import 机制假设的一些不变量。

特别是如果子模块确实使用相对导入引用了其他子模块,则导入将失败,并且在其他情况下它不会正确运行。另一个失败的例子是你必须处理腌制的物体。如果您使用导入为 pandas.io.clipboard 的模块腌制了一些对象,您将能够使用如上所述导入的模块 clipboard 取消腌制它们。

总之,不要!我建议:

如果导入包所花费的时间不是一个真正的问题,请接受它。 或者:尝试寻找替代品。如果您只需要pandas.io.clipboard 而不需要pandas 的其余部分,那么您首先不应该使用pandas,而应该使用仅实现clipboard 功能的较小包。

如果您查看pandas.util.clipboard source code,您会发现它实际上只是pyperclip 模块版本1.7。您可以在site-packages 中添加此模块并使用它来代替pandas 提供的模块。事实上pandas 团队只是在源代码末尾添加了以下部分:

## pandas aliases
clipboard_get = paste
clipboard_set = copy

扩展一下为什么 python 导入是这样工作的。

正如你在 python 中所知道的那样,模块就是对象包也是模块,虽然不是每个模块都是包。当你导入一个包时:

import pandas.io.clipboard

Python 必须:

    创建module 实例pandas 创建module 实例io 并将其作为属性添加到pandas 创建module 实例clipboard 并将其作为属性添加到io

为了创建module实例python必须执行模块中的代码

表单的导入:

from pandas.io import clipboard

只是语法糖:

import pandas.io.clipboard
clipboard = pandas.io.clipboard
del pandas.io

请注意,在from 的情况下,clipboard 可以是module/包,也可以只是在io 中定义的东西。为了检查这一点,解释器必须还导入io,为此它必须还导入pandas

【讨论】:

有没有办法让包的__init__.py 知道如何它是导入的?例如,熊猫的__init__.py 有没有办法知道它是从from pandas.util import clipboardimport pandas 的原始语句中加载的?这将允许包开发人员允许从不需要包其余部分的任何特定模块导入,例如 constants.py 模块。【参考方案2】:

我找到了一个使用sys.meta_path钩住导入过程的方法:

import imp, sys

class DummyLoader(object):

    def load_module(self, name):
        names = name.split("__")
        path = None
        for name in names:
            f, path, info = imp.find_module(name, path)
            path = [path]
        return imp.load_module(name, f, path[0], info)

    def find_module(self, name, path=None):
        if "__" in name and not name.startswith("__"):
            return DummyLoader()
        else:
            return None

if not sys.meta_path:
    sys.meta_path.append(DummyLoader())
else:
    sys.meta_path[0] = DummyLoader()

使用“__”而不是“。”仅用于加载文件:

import pandas__util__clipboard as clip

或使用函数加载文件:

import imp

def load_module(name):
    names = name.split(".")
    path = None
    for name in names:
        f, path, info = imp.find_module(name, path)
        path = [path]
    return imp.load_module(name, f, path[0], info)    

clip = load_module("pandas.util.clipboard")

【讨论】:

【参考方案3】:

使用 Python3.5+,您可以直接从其路径导入源文件,而无需在包含该文件的目录中执行 __init__。灵感来自official importlib example:

import importlib
import os
import pathlib
import sys
import types


def import_module_from_path(path: str | os.PathLike) -> types.ModuleType:
  """Import a module from the given path."""
  module_path = pathlib.Path(path).resolve()
  module_name = module_path.stem  # 'path/x.py' -> 'x'
  spec = importlib.util.spec_from_file_location(module_name, module_path)
  module = importlib.util.module_from_spec(spec)
  sys.modules[module_name] = module
  spec.loader.exec_module(module)
  return module


# Import `my_module` without executing `/path/to/__init__.py`
my_module = import_module_from_path('/path/to/my_module.py')

一些注意事项:

sys.modules[module_name] = module 将覆盖具有相似名称的模块。未来的import module_name 调用将返回此模块。 即使文件已经导入也会执行。

您可以查看sys.modules 并添加一些逻辑来解决这些问题。

【讨论】:

我得到了AttributeError: module 'importlib' has no attribute 'util',但用from importlib import util解决了它,然后使用util.spec_from_file_location()util.module_from_spec()【参考方案4】:

我尝试了这些方法,但无法让它们发挥作用。所以显然设计它不应该工作..

如果您必须这样做,请在您尝试导入的存储库中创建一个新分支,或初始化一个存储库:

git checkout -b without_init

..然后删除__init__.py

从您尝试导入的任何地方,您都可以检查 Python 是否在正确的分支上,如下所示:

import subprocess
print ("Current branch is:", subprocess.check_output(["git rev-parse --abbrev-ref HEAD"], shell=True).strip().decode())

>> without_init

【讨论】:

以上是关于如何在包中只导入没有 exec __init__.py 的子模块的主要内容,如果未能解决你的问题,请参考以下文章

Python:导入包含的包

在包中定义类

即使使用 __init__.py,如何修复“尝试在非包中进行相对导入”

Python包中__init__.py作用

python包中__init__.py的作用

Python包中 __init__.py文件的作用