Python闭包能否替代`__all__`?
用闭包来限制一个Python模块暴露的名称,是否是个好主意?这样可以防止程序员不小心用错模块的名称,比如说用到 import urllib; urllib.os.getlogin()
,同时也能避免像 from x import *
这样造成的命名空间污染,这就是 __all__
的作用。
def _init_module():
global foo
import bar
def foo():
return bar.baz.operation()
class Quux(bar.baz.Splort): pass
_init_module(); del _init_module
对比一下使用 __all__
的同一个模块:
__all__ = ['foo']
import bar
def foo():
return bar.baz.operation()
class Quux(bar.baz.Splort): pass
函数可以采用这种风格来避免污染模块的命名空间:
def foo():
import bar
bar.baz.operation()
这对一个大型包可能会很有帮助,因为它可以帮助用户在交互式查看时区分它的API和包内其他模块的API。另一方面,也许IPython应该在自动补全时区分 __all__
中的名称,更多的用户应该使用可以在文件间跳转的IDE,这样他们就能看到每个名称的定义。
3 个回答
好的,我开始对这个问题有点理解了。闭包确实可以用来隐藏一些私有的东西。下面是一个简单的例子。
没有闭包的情况:
# module named "foo.py"
def _bar():
return 5
def foo():
return _bar() - 2
有闭包的情况:
# module named "fooclosure.py"
def _init_module():
global foo
def _bar():
return 5
def foo():
return _bar() - 2
_init_module(); del _init_module
使用示例:
>>> import foo
>>> dir(foo)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '_bar', 'foo']
>>>
>>> import fooclosure
>>> dir(fooclosure)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'foo']
>>>
其实这个问题有点微妙。在第一种情况下,函数 foo()
只是简单地引用了 _bar()
的名字,如果你把 _bar()
从命名空间中移除,foo()
就会停止工作。每次运行 foo()
时,它都会查找 _bar()
。
相比之下,使用闭包的 foo()
即使 _bar()
不在命名空间中也能正常工作。我甚至不太确定它是怎么工作的……是持有对 _bar()
创建的函数对象的引用,还是持有一个仍然存在的命名空间的引用,这样它就能查找 _bar()
的名字并找到它?
使用 from x import *
这个写法的问题在于,它可能会隐藏一些叫做 NameErrors
的错误,这样会让一些简单的bug变得难以找到。所谓的“命名空间污染”,就是往命名空间里添加一些你根本不知道从哪里来的东西。
这其实和你的闭包(closure)有点像。而且,这样的写法可能会让一些开发工具,比如IDE、代码大纲、pylint等感到困惑。
至于用“错误”的名字来引用模块,这其实也不是个大问题。无论你从哪里导入,模块对象都是一样的。如果这个“错误”的名字在更新后消失了,程序员应该能明白原因,并在下次做得更好。但这不会导致bug的出现。
我喜欢写尽可能简单明了的代码。
__all__
是 Python 的一个功能,专门用来解决模块中哪些名字可以被看到的问题。当你使用它的时候,别人立刻就能明白你在做什么。
你的闭包技巧非常不常见,如果我遇到它,我可能不会立刻理解。你需要写很长的注释来解释它,然后还得再写一段长长的注释来说明为什么要这样做,而不是用 __all__
。
编辑:现在我对问题有了更好的理解,这里有一个替代的答案。
在 Python 中,给模块里的私有名字加个下划线是个好习惯。如果你使用 from the_module_name import *
,你会得到所有不以下划线开头的名字。所以,我更希望看到的是正确使用下划线的方式,而不是闭包技巧。
注意,如果你使用了以下划线开头的名字,甚至不需要使用 __all__
。