如何在Python模块中执行导入而不污染其命名空间?
我正在开发一个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 个回答
整体导入模块的方法是:import foreignmodule
。你觉得这是个缺点,其实这是个优点。因为在代码前面加上模块名,可以让你的代码更容易维护,也更容易理解。
想象一下,六个月后你再看一行代码,比如foo = Bar(baz)
,你可能会想:这个Bar
是哪个模块的?但如果你写成foo = cleverlib.Bar
,那就不那么神秘了。
当然,导入的模块越少,这个问题就越小。对于那些依赖很少的小程序来说,这其实没什么大不了的。
当你开始问这些问题时,想想什么能让代码更容易理解,而不是让代码更容易写。你只需要写一次代码,但你会读很多次。
我见过的一种技巧,包括在标准库中,就是使用 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
你可以照常使用 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()
来区分导入的内容和模块中实际定义的内容了。