Python闭包是否是`__all__`的良好替代品?

3

使用闭包代替 __all__ 来限制 Python 模块暴露的名称是一个好主意吗?这可以防止程序员意外使用模块的错误名称(import urllib; urllib.os.getlogin()),以及避免 "from x import *" 的命名空间污染。

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。

第二段代码不使用第一段代码,它使用 __all__ 来定义相同的模块。 - joeforker
我同意jdb的观点:这些都有多大的问题呢?你的_init_module方法非常奇怪,我不明白你为什么要费心去写它。例如,有多少开发者会无意中使用urllib.os.getlogin? 只需编写您的代码并解决实际问题即可。 - Ned Batchelder
我不理解你对闭包优势的解释。闭包如何“防止程序员意外使用模块的错误名称”?如果有人使用import foo as bar,那么他们得到的名称是bar,如果他们使用import foo,则得到的名称是foo。闭包有什么不同之处? - steveha
我只有在使用Plone时遇到这些问题的经验,因为它有很多代码,没有人熟悉所有代码,特别是集成者。在你知道之前,足够多的集成者通过内省发现了一个特定的名称,如果你决定从你的模块中删除例如from zope.app.intid.interfaces import IIntIds,那么你会破坏他们的模块。当从IPython shell内省API时,很难知道任何特定的模块级名称是接口还是实现。 - joeforker
据我所知,在Python模块中使用前导下划线表示私有的实现是一种良好的编程习惯。Plone应该更改其模块,以便非公共的内容以下划线开头。人们仍然可以检查名称,但至少他们会知道自己在冒险。 - steveha
显示剩余2条评论
3个回答

7

我喜欢编写尽可能简单易懂的代码。

__all__ 是 Python 的一个特性,专门用来解决限制模块中可见名称的问题。当你使用它时,人们立即就能理解你在做什么。

您的闭包技巧非常不标准,如果我遇到它,我会不能立即理解。你需要添加一个长注释来解释它,然后再添加另一个长注释来解释为什么要这样做,而不是使用__all__

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

在 Python 中,使用下划线前缀表示模块中的私有名称被认为是一种良好的惯例。如果您执行from_the_module_name import *,您将获得所有未以下划线开头的名称。因此,与其使用闭包技巧,我更喜欢看到正确使用下划线前缀惯例的方式。

请注意,如果您使用下划线前缀名称,则甚至不需要使用__all__


问题在于__all__只有在使用from x import *时才有帮助,而这本来就是不应该做的事情,而闭包技巧提供了非常简洁的内省。在Plone中,任何给定名称都是在数百个egg、数万个python文件和数百万行代码中的某个地方定义的,你实际上读我的代码并有机会注意到__all__或闭包的可能性很小。 - joeforker

4
from x import * 的问题在于它可能会隐藏 NameErrors,从而使得微不足道的错误难以追踪。 "命名空间污染" 意味着向你不知道其来源的命名空间中添加内容。

这也是你的闭包所做的事情。此外,它可能会混淆 IDE、大纲、pylint 等工具。

使用 "错误" 的模块名称也不是真正的问题。无论从哪里导入模块对象都是相同的。如果 "错误" 名称消失了(经过更新后),程序员应该清楚为什么并激励他下次正确地操作。但它不会导致错误。


1
from x import * 应该只在 REPL 中使用。脆弱的代码不是一个 bug 吗? - joeforker

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()并找到它?


请参考https://dev59.com/0XRB5IYBdhLWcg3wCjjO,了解闭包的更常规用法。在Python 2.x中,foo()将_bar保留在foo.func_closure中。 - joeforker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接