首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在“`package/_init__. at”中提供函数,如何防止模块内部代码从顶级`__init__.py`中导入?

在“`package/_init__. at”中提供函数,如何防止模块内部代码从顶级`__init__.py`中导入?
EN

Stack Overflow用户
提问于 2022-03-26 17:22:45
回答 3查看 1K关注 0票数 0

我正在编写一个基于蟒蛇熊猫的数据处理包。对于具有功能风格的部分,我想使我的包的层次结构更平顺。目前,需要使用以下调用导入函数:

代码语言:javascript
复制
from package.module.submodule import my_function

拟议的修改将使进口成为可能。

代码语言:javascript
复制
from package import my_function

为了实现这一点,函数和其他对象将被导入到package/__init__.py中,以便它们可以在顶级名称空间中使用。大熊猫就是这样做的,例如__init__.py使进口成为可能。

代码语言:javascript
复制
from pandas import DataFrame

实际上,DataFrame类是在pandas.core.frame中定义的。通常您必须像这样导入它:from pandas.core.frame import DataFrame,但是由于它是在顶级__init__.py中导入的,所以它在顶层可用。

将功能作为顶级导入提供:

  • 将为用户公开平面层次结构,并使使用包更容易。
  • 但是在内部(在包代码中),我们不应该直接从package/__init__.py导入以避免创建循环引用。
代码语言:javascript
复制
- [Searching for from+pandas+import](https://github.com/pandas-dev/pandas/search?p=5&q=from+pandas+import) It seems that pandas always avoids importing from the top level (except test scripts which do use from pandas import DataFrame). I don't know how to enforce this.
- Maybe this tool can be helpful: [pylint-forbidden-imports](https://pypi.org/project/pylint-forbidden-imports/),
- or rather [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) since we are using black and flake8 as a pre commit hook. flake8-tidy-imports makes it possible to define which imports are forbidden. It seems it applies to the whole package though, and not to a specific location in the package.

相关问题

  • .py进口
  • 用Python?
    • 被接受的答案提到“我在开发生命周期早期为模块编写了一个__all__,以便其他可能使用我的代码的人知道他们应该使用什么,而不是使用什么。”
EN

回答 3

Stack Overflow用户

发布于 2022-04-02 02:40:08

我认为您所表达的关注是,“导入子模块”和“在模块导入过程中导入子模块”并不是一回事。例如,用ipython编写它:

代码语言:javascript
复制
from module.sub.file import func

并从module包中编写这篇文章

代码语言:javascript
复制
from module.sub.file import func

不要做同样的事情(即使他们看起来是一样的)。这是因为如果module已经启动了它的初始化;那么后续对子模块的调用将不会重新初始化modulemodule也不需要在调用它的子模块之前完成初始化。这与类继承的工作方式非常相似。

这意味着包从所有“子模块”中提取各种函数是完全有效的,而每个子模块都可以通过包本身显式地相互导入,而不会导致无限循环。这是故意的。例如,

代码语言:javascript
复制
module
    __init__.py
        from .sub1.file1 import func1
        from .sub2.file2 import func2
    sub1
        __init__.py
        file1.py
            from module.sub2.file2 import func2
            def func1(x):
                return func2(x)+x
    sub2
        __init__.py
        file2.py
            def func2(x):
                return x+1

在这里,子模块sub1依赖于sub2。行from module.sub2.file2 import func2通常意味着

  1. 执行module/__init__.py并从命名空间sub2加载
  2. 执行module/sub2/__init__.py并从命名空间file2加载
  3. 执行module/sub2/file2.py并从命名空间func2加载

但是在from module import func1的调用过程中,当我们到达file1.py of from module.sub2.file2 import func2中的线路时,我们要么已经运行,要么正在运行module/__init__.pymodule/sub/__init__.py。这意味着这一行更像是:

  1. module/__init__.py目前是executed...skip
  2. module/sub2/__init__.py已经是loaded...skip了
  3. 执行module/sub2/file2.py并从命名空间func2加载

通常,如果当前正在执行module/__init__.py,则只需跳过对该语句的额外调用。您完全可以直接跳过import module本身,即使它还没有完成加载。添加一些打印语句

代码语言:javascript
复制
module
    __init__.py
        print('start init module')
        from .sub1.file1 import func1
        from .sub2.file2 import func2
        print('end init module')
    sub1
        __init__.py
        file1.py
            print('loading module from file1.py')
            import module
            print('done loading module from file1.py')
            from module.sub2.file2 import func2
            def func1(x):
                return func2(x)+x
    sub2
        __init__.py
        file2.py
            def func2(x):
                return x+1

现在运行from module import func1

代码语言:javascript
复制
start init module
loading module from file1.py
# Notice that nothing is printed here, meaning module/__init__.py was not run again, 
# even though we explicitly wrote "import module", additionally "module" wasn't 
# even finished executing it's own __init__.py file. 
done loading module from file1.py
end init module

从设计的角度来看,这是很棒的。这意味着sub2很可能是一个完全独立于module但依赖于module的包。然后,在某个时候,有人会说,“让我们把这个附属包作为子模块丢到我们的module包中”。整个文件夹被放入其中(不改变任何代码),然后module就可以导入它,就像它是本地的子包一样,而不需要意外地创建导入循环,即使子包依赖于模块本身的其他部分。

票数 1
EN

Stack Overflow用户

发布于 2022-04-05 04:26:53

您的问题正是在文档中阐明的,并通过使用包内引用来解决。引用子模块时使用

代码语言:javascript
复制
from ..frame import DataFrame

而不是使用

代码语言:javascript
复制
from pandas.core.frame import DataFrame

我可以看到,它也适用于人们,这里

这种类型的引用是正常使用的,这是百度团队在OCR引擎中对导入所有模块的一个例子。

坚持你的想法,因为如果你的用户是初学者的话,引用其他路径是很难的。

票数 1
EN

Stack Overflow用户

发布于 2022-03-29 21:51:11

我已经搜索过熊猫GitHub存储库,无法找到解决您特定问题的预提交钩子,所以我从熊猫储存库中调整了堆芯阵列钩子。

我的测试设置具有以下文件夹结构(不包括.git文件夹):

代码语言:javascript
复制
├── package
│   ├── __init__.py
│   ├── module
│   │   └── submodule.py
│   └── second_module
│       └── submodule.py
├── .pre-commit-config.yaml
└── scripts
   └── import_from_submodules.py

.pre-commit-config.yaml包含

代码语言:javascript
复制
repos:
-   repo: local
    hooks:
    -   id: import-from-submodules
        name: Import from appropriate submodules
        language: python
        entry: python scripts/import_from_submodules.py package
        files: ^package/
        types: [python]

并且import_from_submodules.py文件包含

代码语言:javascript
复制
"""
Check that all imports reference the correct submodule and not import directly
from __init__.py, even though that is technically possible.

This is meant to be run as a pre-commit hook - to run it manually, you can do:

    pre-commit run import-from-submodules --all-files

"""

from __future__ import annotations

import argparse
import ast
import sys
from typing import Sequence


class Visitor(ast.NodeVisitor):
    def __init__(self, package_name: str, path: str) -> None:
        self.package_name = package_name
        self.path = path
        self.error_message = (
            "{path}:{lineno}:{col_offset}: "
            f"Don't import from {self.package_name}, "
            f"import from {self.package_name}.submodule instead\n"
        )

    def visit_Import(self, node: ast.Import) -> None:
        if any(module.name == self.package_name for module in node.names):
            msg = self.error_message.format(
                path=self.path, lineno=node.lineno, col_offset=node.col_offset
            )
            sys.stdout.write(msg)
            sys.exit(1)
        super().generic_visit(node)

    def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
        if node.module == self.package_name:
            msg = self.error_message.format(
                path=self.path, lineno=node.lineno, col_offset=node.col_offset
            )
            sys.stdout.write(msg)
            sys.exit(1)
        super().generic_visit(node)


def import_from_submodules(package_name: str, content: str, path: str) -> None:
    tree = ast.parse(content)
    visitor = Visitor(package_name, path)
    visitor.visit(tree)


def main(argv: Sequence[str] | None = None) -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("package_name")
    parser.add_argument("paths", nargs="*")
    args = parser.parse_args(argv)

    for path in args.paths:
        with open(path, encoding="utf-8") as fd:
            content = fd.read()
        import_from_submodules(args.package_name, content, path)


if __name__ == "__main__":
    main()

这使用ast模块解析package目录中每个Python文件的python源代码,并访问每个import <module>from <module> import <function>语句。如果<module>部件等于包名(这是您可以在预提交配置中设置的脚本的命令行参数),则打印违规行的位置,脚本退出时使用非零退出代码来指示存在错误。

假设package/module/submodule.py中有一个函数package/module/submodule.py,它也是在__init__.py中导入并包含在__all__中的。在package/second_module/submodule.py内部,如果运行pre-commit run import-from-submodules --all-files,下面的行将引发错误

代码语言:javascript
复制
import package
from package import fun
from ..package import fun

鉴于

代码语言:javascript
复制
from package.module.submodule import fun
from ..module.submodule import fun

别说了。请注意,相对导入示例说明,在将模块名与给定包名进行比较时,将忽略相对导入的所有前导点。

我希望这涵盖了你的用例。当然,欢迎您将错误消息更改为更有用/更清晰的信息。如果您想扩展这段代码,ast模块是非常强大的。例如,前面提到的堆芯阵列预提交钩子还通过检查每个属性访问来标记所有表达式pd.array

票数 -1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71630256

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档