Python闭包能否替代`__all__`?

3 投票
3 回答
801 浏览
提问于 2025-04-15 15:25

用闭包来限制一个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 个回答

1

好的,我开始对这个问题有点理解了。闭包确实可以用来隐藏一些私有的东西。下面是一个简单的例子。

没有闭包的情况:

# 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() 的名字并找到它?

4

使用 from x import * 这个写法的问题在于,它可能会隐藏一些叫做 NameErrors 的错误,这样会让一些简单的bug变得难以找到。所谓的“命名空间污染”,就是往命名空间里添加一些你根本不知道从哪里来的东西。

这其实和你的闭包(closure)有点像。而且,这样的写法可能会让一些开发工具,比如IDE、代码大纲、pylint等感到困惑。

至于用“错误”的名字来引用模块,这其实也不是个大问题。无论你从哪里导入,模块对象都是一样的。如果这个“错误”的名字在更新后消失了,程序员应该能明白原因,并在下次做得更好。但这不会导致bug的出现。

7

我喜欢写尽可能简单明了的代码。

__all__ 是 Python 的一个功能,专门用来解决模块中哪些名字可以被看到的问题。当你使用它的时候,别人立刻就能明白你在做什么。

你的闭包技巧非常不常见,如果我遇到它,我可能不会立刻理解。你需要写很长的注释来解释它,然后还得再写一段长长的注释来说明为什么要这样做,而不是用 __all__

编辑:现在我对问题有了更好的理解,这里有一个替代的答案。

在 Python 中,给模块里的私有名字加个下划线是个好习惯。如果你使用 from the_module_name import *,你会得到所有不以下划线开头的名字。所以,我更希望看到的是正确使用下划线的方式,而不是闭包技巧。

注意,如果你使用了以下划线开头的名字,甚至不需要使用 __all__

撰写回答