当我编写 python 库时,我经常会得到这样的结果:
/my_lib
__init__.py
module_a.py
module_b.py
...
现在假设
module_a
和 module_b
非常大,并且定义了很多公共和非公共方法。例如:
import ...
from ... import ...
... etc
__all__ = [
"PublicClass",
"public_fn",
"PUBLIC_STATIC_VAR",
"OtherPublicClass",
...
]
class _PrivateClass:
...
我花了很多精力精心策划了一个经过深思熟虑的符号列表,以便在
__all__
中导出。现在在我的__init__.py
中我想这样做(当然这是行不通的):
from my_lib.module_a import * as module_a
from my_lib.module_b import * as module_b
from third_party_lib import * as third_party
from other_lib import OtherA, OtherB
__all__ = [
*module_a,
*module_b,
*third_party,
"OtherA",
"OtherB",
]
本质上,我想要的是,如果有人导入
my_lib
,他们可以从同一命名空间访问 module_a
、module_b
和 third_party
中的所有公共符号。即:
import my_lib
obj1 = my_lib.pub_fn_from_module_a()
obj2 = my_lib.PubClassFromModuleB()
obj3 = my_lib.CONSTANT_FROM_THIRD_PARTY_LIB
obj4 = my_lib.OtherA()
obj5 = my_lib.OtherB()
__all__
我知道我能做到:
from my_lib.module_a import *
from my_lib.module_b import *
from third_party_lib import *
from other_lib import OtherA, OtherB
# __all__ is not defined in __init__.py
这将隐式定义
__all__
并实现所需的行为,但静态代码分析工具无法将其与死代码区分开来。例如,预提交挂钩或代码格式化工具可能会删除这些导入。
我也可以:
from my_lib.module_a import (
# <type all public symbols from module_a here>
)
from my_lib.module_b import (
# <type all public symbols from module_b here>
)
from third_party_lib import (
# <type all public symbols from third_party_lib here>
)
from other_lib import OtherA, OtherB
__all__ = [
# <copy-paste the body of module_a.__all__ here>
# <copy-paste the body of module_b.__all__ here>
# <type all public symbols from third_party_lib here>
"OtherA",
"OtherB",
]
但现在我基本上必须在
module_a
中重新输入每个符号的名称三次,一次在module_a.__all__
中,一次在__init__.py
的导入中,一次在__all__
中的__init__.py
定义中。 module_b
也是如此,我还需要确保所有 3 在开发过程中保持同步。
__all__
这种方法可行,但看起来真的很尴尬:
from my_lib.module_a import * as module_a
from my_lib.module_b import * as module_b
from my_lib import module_a, module_b
from third_party_lib import * as third_party
from other_lib import OtherA, OtherB
# only purpose of `_keep` is to fake the usage of a few symbols
# such that the star imports are not flagged as dead code.
# It does nothing at runtime.
def _keep(x): pass
_keep(random_symbol_from_module_a)
_keep(random_symbol_from_module_b)
_keep(random_symbol_form_third_party)
__all__ = [
*module_a.__all__,
*module_b.__all__,
*third_party_lib.__all__,
"OtherA",
"OtherB",
]
需要导入星星 (
from ... import *
),因为例如pycharm 或其他静态代码分析工具无法推断 *module_a.__all__
语句的作用。因此,如果没有明星导入,静态代码分析工具将报告到处都缺少导入。
您想要的代码模式的一个很好的示例可以在标准库的
__init__
包的 asyncio
模块中找到:
https://github.com/python/cpython/blob/1b1f8398d0ffe3c8ba2cca79d0c0f19a6a34e72a/Lib/asyncio/__init__.py
from .base_events import *
from .coroutines import *
from .events import *
from .exceptions import *
from .futures import *
from .locks import *
from .protocols import *
from .runners import *
from .queues import *
from .streams import *
from .subprocess import *
from .tasks import *
from .taskgroups import *
from .timeouts import *
from .threads import *
from .transports import *
__all__ = (base_events.__all__ +
coroutines.__all__ +
events.__all__ +
exceptions.__all__ +
futures.__all__ +
locks.__all__ +
protocols.__all__ +
runners.__all__ +
queues.__all__ +
streams.__all__ +
subprocess.__all__ +
tasks.__all__ +
taskgroups.__all__ +
threads.__all__ +
timeouts.__all__ +
transports.__all__)
如您所见,它采用与解决方案 #3 基本相同的方法,但它不是解压所有
__all__
列表,而是使用 +
运算符来加入它们,事实证明,大多数静态分析器都装备得更好静态推断与 +
运算符连接的列表的结果值,因此通过这种方法,静态分析器不会抱怨到处都缺少导入。