导入 __init__.py 中的所有内容时排除模块

act*_*nda 4 python python-import

问题

考虑以下布局:

package/
    main.py
    math_helpers/
        mymath.py
        __init__.py
Run Code Online (Sandbox Code Playgroud)

mymath.py包含:

import math

def foo():
    pass
Run Code Online (Sandbox Code Playgroud)

我希望main.py能够使用mymath.py这样的代码:

import math_helpers
math_helpers.foo()
Run Code Online (Sandbox Code Playgroud)

为此,__init__.py包含:

from .mymath import *
Run Code Online (Sandbox Code Playgroud)

然而,导入的模块mymath.py现在位于math_helpers命名空间中,例如math_helpers.math可以访问。


目前的方法

我在 的末尾添加以下内容mymath.py

import types
__all__ = [name for name, thing in globals().items()
          if not (name.startswith('_') or isinstance(thing, types.ModuleType))]
Run Code Online (Sandbox Code Playgroud)

这似乎可行,但这是正确的方法吗?

Mad*_*ist 7

一方面,有很多充分的理由不进行明星导入,但另一方面,Python 是为同意的成年人准备的。

__all__是确定明星导入中显示内容的推荐方法。您的方法是正确的,完成后您可以进一步清理命名空间:

import types
__all__ = [name for name, thing in globals().items()
          if not (name.startswith('_') or isinstance(thing, types.ModuleType))]
del types
Run Code Online (Sandbox Code Playgroud)

虽然不太推荐,但您也可以直接从模块中清理元素,这样它们就根本不会出现。如果您需要在模块中定义的函数中使用它们,这将是一个问题,因为每个函数对象都有一个__globals__绑定到其父模块的__dict__. 但是,如果您只导入math_helperscall math_helpers.foo(),并且不需要在模块中的其他位置持久引用它,则可以简单地在最后取消链接:

del math_helpers
Run Code Online (Sandbox Code Playgroud)

长版

模块导入在模块的__dict__. 任何在顶层绑定的名称,无论是通过类定义、函数定义、直接赋值还是其他方式,都存在于该字典中。有时,需要清理中间变量,正如我建议的那样types

假设您的模块如下所示:

测试模块.py

import math
import numpy as np

def x(n):
    return math.sqrt(n)

class A(np.ndarray):
    pass

import types
__all__ = [name for name, thing in globals().items()
           if not (name.startswith('_') or isinstance(thing, types.ModuleType))]
Run Code Online (Sandbox Code Playgroud)

在这种情况下,__all__将是['x', 'A']. 但是,模块本身将包含以下名称:'math', 'np', 'x', 'A', 'types', '__all__'.

如果您del types在最后运行,它将从命名空间中删除该名称。显然这是安全的,因为一旦构建types就不会在任何地方被引用。__all__

同样,如果你想np通过添加来删除del np,那就可以了。该类A是由模块代码末尾完全构造的,因此不需要全局名称np来引用其父类。

则不然math。如果您要del math在模块代码的末尾执行此操作,则该函数x将无法工作。如果导入模块,您可以看到该x.__globals__模块的__dict__

import test_module

test_module.__dict__ is test_module.x.__globals__
Run Code Online (Sandbox Code Playgroud)

如果你math从模块字典中删除并调用test_module.x,你会得到

NameError: name 'math' is not defined
Run Code Online (Sandbox Code Playgroud)

因此,在某些非常特殊的情况下,您可能能够清理 的命名空间mymath.py,但这不是推荐的方法,因为它仅适用于某些情况。

总之,坚持使用__all__

一个相关的故事

有一次,我有两个模块实现了类似的功能,但针对不同类型的最终用户。我想将几个函数从 module 复制a到 module 中b。问题是我希望这些函数像在 module 中定义的那样工作b。不幸的是,它们依赖于 中定义的常量ab定义了自己的常量版本。例如:

a.py

value = 1

def x():
    return value
Run Code Online (Sandbox Code Playgroud)

b.py

from a import x

value = 2
Run Code Online (Sandbox Code Playgroud)

我想b.x访问b.value而不是a.value. 我通过添加以下内容来实现这一点b.py(基于/sf/answers/945229421/):

import functools, types

x = functools.update_wrapper(types.FunctionType(x.__code__, globals(), x.__name__, x.__defaults__, x.__closure__), x)
x.__kwdefaults__ = x.__wrapped__.__kwdefaults__
x.__module__ = __name__
del functools, types
Run Code Online (Sandbox Code Playgroud)

我为什么要告诉你这一切?好吧,您可以创建一个在命名空间中没有任何杂散名称的模块版本。不过,您将无法看到函数中全局变量的更改。这只是将 python 推向其正常用法之外的一个练习。我强烈不建议这样做,但这里有一个示例模块,可以有效地冻结其__dict__功能。它具有与上面相同的成员test_module,但全局命名空间中没有模块:

import math
import numpy as np

def x(n):
    return math.sqrt(n)

class A(np.ndarray):
    pass

import functools, types, sys

def wrap(obj):
    """ Written this way to be able to handle classes """
    for name in dir(obj):
        if name.startswith('_'):
            continue
        thing = getattr(obj, name)
        if isinstance(thing, FunctionType) and thing.__module__ == __name__:
            setattr(obj, name,
                    functools.update_wrapper(types.FunctionType(thing.func_code, d, thing.__name__, thing.__defaults__, thing.__closure__), thing)
            getattt(obj, name).__kwdefaults__ = thing.__kwdefaults__
        elif isinstance(thing, type) and thing.__module__ == __name__:
            wrap(thing)

d = globals().copy()
wrap(sys.modules[__name__])
del d, wrap, sys, math, np, functools, types
Run Code Online (Sandbox Code Playgroud)

所以,是的,请永远不要这样做!但如果你这样做了,请将它放在某个实用程序类中。