如何将子模块名称排除在Python包的命名空间之外?

13 投票
4 回答
3585 浏览
提问于 2025-04-16 13:27

我想让某个模块的接口里包含一些特定的函数和类(就这些,不要别的)。我可以把这些都放在一个文件里,这样就能轻松实现我想要的接口。但是因为代码很多,我更希望把它们分成几个文件,比如说:

mypackage/
    __init__.py
    a.py
    b.py
    c.py
    d.py

为了实现我想要的接口,我在这个包里定义了一个 __init__.py 文件,里面导入了 abcd 的所有公共符号:

from a import func_a1, func_a2, ClassA1, ClassA2
from b import func_b1, func_b2, ClassB1, ClassB2
from c import func_c1, func_c2, ClassC1, ClassC2
from d import func_d1, func_d2, ClassD1, ClassD2

如果我用下面的方式导入这个包:

import mypackage

那么这个包的命名空间里也会包含 abcd 这些符号。这些名字是实现细节,不是我想要的接口的一部分。我不想让它们作为“公共”符号出现。有没有什么好的办法可以把它们去掉呢?

我考虑过的选项有:

  1. 使用一个单独的模块,而不是包。接口看起来没问题,但实现会变得不太清晰。

  2. __init__.py 的末尾添加这一行:

    del a, b, c, d
    

    这样可以,但感觉有点像是变通方法。(比如说,你不能再 import __init__,而没有这一行是可以的。)

  3. abcd 重命名为 _a_b_c_d。这样它们就作为“私有”符号包含在 mypackage 的命名空间里,我对此没问题,但所有文件名都以下划线开头感觉有点奇怪(实际上,当然还有超过四个子模块)。

有没有更好的建议?或者对哪个选项更好的看法?

还是说我太过于纠结,不应该在意这些?

4 个回答

0

这里有一个灵感来自于JavaScript单功能模块的解决方案:

def __init__module():
    from os import path

    def _module_export_1():
        return path.abspath('../foo')

    def _module_export_2():
        return path.relpath('foo/bar', 'foo')

    g = globals()
    g['module_export_1'] = _module_export_1
    g['module_export_2'] = _module_export_2

__init__module()

虽然这个模块需要从os中导入'path',但'path'并不会污染模块的命名空间。模块命名空间里唯一的杂物是__init_module(),而且它的双下划线前缀清楚地标明了这是私有的。

另一种选择是在每个函数的顶部导入需要的模块,而不是在模块的顶部导入。第一次导入模块后,后续的导入其实只是查找sys.modules字典。

不过我同意这里其他评论者的看法——Python的惯例是不太担心模块命名空间的污染,而是要让模块的用户清楚哪些部分是你公开的API,哪些是内部使用的。

13

如果一个包里的某些文件确实是实现细节,那就可以在它们前面加个下划线——这就是我们这么做的原因。

比如,你看看 ctypes,你会看到

__init__.py
==================================================
"""create and manipulate C data types in Python"""

import os as _os, sys as _sys

__version__ = "1.1.0"

from _ctypes import Union, Structure, Array
from _ctypes import _Pointer
from _ctypes import CFuncPtr as _CFuncPtr
...

正如你所看到的,连 ossys 在那个文件里也成了实现细节。

6

如果你真的想把命名空间里的名字去掉,你只需要用 del 这个命令,名字就会像风一样消失。

撰写回答