如何在Python模块中执行导入而不污染其命名空间?

39 投票
6 回答
17390 浏览
提问于 2025-04-17 02:19

我正在开发一个Python包,用来处理一些科学数据。在这个包的每个模块中,我几乎都需要用到其他模块和包中一些常用的类和函数,包括numpy。

那么,处理这些类和函数的Pythonic(符合Python风格)方式是什么呢?我考虑了几种不同的方法,但每种都有自己的缺点。

  • 在模块级别导入类,比如用 from foreignmodule import Class1, Class2, function1, function2
    这样导入的函数和类在每个函数中都很容易访问。但另一方面,这样会让模块的命名空间变得杂乱,使得用 dir(package.module)help(package.module) 查看时,导入的函数会让内容显得拥挤。

  • 在函数级别导入类,比如用 from foreignmodule import Class1, Class2, function1, function2
    这样函数和类也很容易访问,而且不会污染模块。但每个函数都要从十几个模块导入,看起来像是重复代码太多了。

  • 在模块级别导入整个模块,比如用 import foreignmodule
    虽然这样污染不多,但每次调用函数或类时都需要加上模块名,显得有点麻烦。

  • 使用一些人为的变通方法,比如用一个函数体来处理所有这些操作,然后只返回需要导出的对象……像这样

    def _export():
        from foreignmodule import Class1, Class2, function1, function2
        def myfunc(x):
            return function1(x, function2(x))
        return myfunc
    myfunc = _export()
    del _export
    

    这样可以解决模块命名空间污染和函数使用方便的问题……但看起来一点也不符合Python的风格。

那么,哪种解决方案是最符合Python风格的呢?有没有其他我忽略的好方法?

6 个回答

11

整体导入模块的方法是:import foreignmodule。你觉得这是个缺点,其实这是个优点。因为在代码前面加上模块名,可以让你的代码更容易维护,也更容易理解。

想象一下,六个月后你再看一行代码,比如foo = Bar(baz),你可能会想:这个Bar是哪个模块的?但如果你写成foo = cleverlib.Bar,那就不那么神秘了。

当然,导入的模块越少,这个问题就越小。对于那些依赖很少的小程序来说,这其实没什么大不了的。

当你开始问这些问题时,想想什么能让代码更容易理解,而不是让代码更容易写。你只需要写一次代码,但你会读很多次。

13

我见过的一种技巧,包括在标准库中,就是使用 import 模块 as _模块from 模块 import 变量 as _变量,也就是把导入的模块或变量命名为以一个下划线开头。

这样做的效果是,其他代码会按照Python的常规做法,把这些成员当作私有的。这种做法即使在不查看 __all__ 的情况下也适用,比如IPython的自动补全功能。

下面是Python 3.3的 random 模块的一个例子:

from warnings import warn as _warn
from types import MethodType as _MethodType, BuiltinMethodType as _BuiltinMethodType
from math import log as _log, exp as _exp, pi as _pi, e as _e, ceil as _ceil
from math import sqrt as _sqrt, acos as _acos, cos as _cos, sin as _sin
from os import urandom as _urandom
from collections.abc import Set as _Set, Sequence as _Sequence
from hashlib import sha512 as _sha512

另一种技巧是在函数内部进行导入,这样它们就变成了局部变量:

"""Some module"""
# imports conventionally go here
def some_function(arg):
    "Do something with arg."
    import re  # Regular expressions solve everything
    ...

这样做的主要原因是,它实际上是一种懒惰的做法,推迟导入模块的依赖,直到它们真的被使用。假设模块中的某个函数依赖于一个特别大的库。如果在文件顶部导入这个库,那么导入这个模块时就会加载整个库。这样一来,导入模块的速度可以很快,只有真正调用那个函数的代码才会承担加载库的成本。此外,如果依赖的库不可用,那些不需要这个功能的代码仍然可以导入模块并调用其他函数。不过,这样做的缺点是,使用函数级别的导入会让你的代码依赖关系变得不那么明显。

下面是Python 3.3的 os.py 的一个例子:

def get_exec_path(env=None):
    """[...]"""
    # Use a local import instead of a global import to limit the number of
    # modules loaded at startup: the os module is always loaded at startup by
    # Python. It may also avoid a bootstrap issue.
    import warnings
26

你可以照常使用 from W import X, Y, Z,然后用 __all__ 这个特殊符号来定义你希望别人从你的模块中导入哪些内容:

__all__ = ('MyClass1', 'MyClass2', 'myvar1', …)

这会定义如果用户使用 import * 从你的模块导入内容时,哪些符号会被导入。

一般来说,Python 程序员不应该使用 dir() 来搞清楚如何使用你的模块,如果他们这样做,可能说明其他地方有问题。他们应该查看你的文档,或者输入 help(yourmodule) 来了解如何使用你的库。或者,他们也可以直接浏览源代码,这样的话 (a) 导入的内容和定义的内容之间的区别就很清楚了,(b) 他们会看到 __all__ 的声明,知道哪些内容是可以使用的。

如果你试图在这种情况下支持 dir(),而这个功能本来就不是为这个任务设计的,你就得对自己的代码施加一些烦人的限制,正如这里其他回答所说的那样。我的建议是:不要这样做!可以参考标准库的做法:它在需要代码清晰和简洁的时候使用 from … import …,并提供 (1) 有用的文档字符串,(2) 完整的文档,以及 (3) 可读的代码,这样就没人需要在模块上运行 dir() 来区分导入的内容和模块中实际定义的内容了。

撰写回答