Python3 中 pkgutil.walk_packages 是否需要 __init__.py 文件?

9
PEP420让__init__.py文件成为可选项:https://docs.python.org/3/whatsnew/3.3.html#pep-420-implicit-namespace-packages 尽管没有这些文件,pkgutil.walk_packages无法按预期运行:https://docs.python.org/3/library/pkgutil.html#pkgutil.walk_packages 请看以下示例:
$ tree foo
foo
├── bar
│   ├── baz.py
│   └── __init__.py
├── __init__.py
└── womp.py

还有一个测试脚本

# test.py
import pkgutil

import foo


for _, mod, _ in pkgutil.walk_packages(foo.__path__, foo.__name__ + '.'):
    print(mod)

在Python2和Python3中,我得到了以下输出:

$ python2.7 test.py
foo.bar
foo.bar.baz
foo.womp
$ python3.5 test.py
foo.bar
foo.bar.baz
foo.womp

删除__init__.py文件,并仅使用python3,我得到了以下结果:

$ find -name '__init__.*' -delete
$ python3.5 test.py
foo.bar

模块绝对可以被导入:
$ python3.5 -c 'import foo.bar.baz'
$

这是一个bug吗?我必须创建__init__.py文件才能实现我的目标吗?

3个回答

3
作为一种解决方法(也许这对其他人有帮助),我使用了类似以下的代码。虽然不完美(如果密码更改或者包不是以"."为根目录,就会出现问题),但是对于我的简单用例来说已经能够实现我想要的功能了:
def walk_modules(pkg):
    assert hasattr(pkg, '__path__'), 'This function is for packages'
    path = pkg.__name__.replace('.', '/')
    modules = []
    for root, _, filenames in os.walk(path):
        for filename in filenames:
            if filename.startswith('.') or not filename.endswith('.py'):
                continue
            path = os.path.join(root, filename)
            modules.append(os.path.splitext(path)[0].replace('/', '.'))
    for module in sorted(modules):
        yield __import__(module, fromlist=['__trash'])

2
对于那些想知道的人,距离第一次提出这个问题已经过去了大约18个月,walk_packages仍然不支持PEP420本地命名空间包。我不得不重新添加__init__.py文件来支持我的walk_packages用例。 - Xuor
4
稍微更复杂的解决方法,FTR: https://github.com/sanitizers/octomachinery/blob/2428877/tests/circular_imports_test.py#L22-L55 - webknjaz -- Слава Україні

1
另一种尊重合并命名空间包 __path__ 属性的方法:
import pkgutil
from pathlib import Path

def iter_packages(path, prefix, onerror=None):
    """ Find packages recursively, including PEP420 packages """
    yield from pkgutil.walk_packages(path, prefix, onerror)
    namespace_packages = {}
    for path_root in path:
        for sub_path in Path(path_root).iterdir():
            # TODO: filter to legal package names
            if sub_path.is_dir() and not (sub_path / '__init__.py').exists():
                ns_paths = namespace_packages.setdefault(prefix + sub_path.name, [])
                ns_paths.append(str(sub_path))
    for name, paths in namespace_packages.items():
        # TODO: construct a loader somehow?
        yield pkgutil.ModuleInfo(None, name, True)
        yield from iter_packages(paths, name + '.', onerror)

-2

PEP420并没有使init.py文件成为可选项,在99%的情况下。 它只是允许通过目录创建“命名空间”包而不需要__init__.py

当您创建常规包时,不应省略__init__.py。 请参见https://dev.to/methane/don-t-omit-init-py-3hga


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接