python的__init__.py

发布于:2025-06-27 ⋅ 阅读:(17) ⋅ 点赞:(0)

在此之前先确认一个概念是否弄清

模块命名空间

1. 目录结构

假设你有以下结构:

testpkg/
  __init__.py
  fool.py
  maybe.py

内容如下:

fool.py

# testpkg/fool.py
class Fool:
    pass

maybe.py

# testpkg/maybe.py
class Maybe:
    pass

__init__.py

(先什么也不写,空文件)


2. 只import子模块,不动__init__.py

在外部写测试代码:

import testpkg.fool
import testpkg.maybe

print(hasattr(testpkg, 'Fool')) # False
print(hasattr(testpkg, 'Maybe')) # False

print(hasattr(testpkg.fool, 'Fool')) # True
print(hasattr(testpkg.maybe, 'Maybe')) # True

解释:

  • testpkg.fool模块的命名空间里有Fool(因为在fool.py里定义了)
  • testpkg.maybe模块的命名空间里有Maybe
  • 但是testpkg(这个包)的命名空间里没有FoolMaybe,因为没把它们导入到包的顶层。

3. “模块命名空间”到底是什么?

  • 每个py文件(模块)加载后,Python会创建一个名字空间(实际是一个dict),存放所有在这个模块里定义的名字。
  • 可以通过模块名.__dict__看到它的命名空间。

例如:

import testpkg.fool
import testpkg.maybe

print(testpkg.fool.__dict__.keys())
print(testpkg.maybe.__dict__.keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'Fool'])
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'Maybe'])

4. 只有import到包,名字才会出现在包的命名空间

如果你修改testpkg/__init__.py为:

from testpkg.fool import Fool
from testpkg.maybe import Maybe

__all__ = ["Fool", "Maybe"]
  • import testpkg 的时候,实际上会执行 testpkg/__init__.py 里的代码。
  • from testpkg.fool import Fool 这行代码的作用是:
    testpkg.fool 这个模块里的 Fool,导入到当前命名空间(也就是 testpkg 的命名空间)下。
  • 同理,from testpkg.maybe import MaybeMaybe 导入到 testpkg 命名空间下。

现在再测试:

import testpkg

print(hasattr(testpkg, 'Fool')) # True
print(hasattr(testpkg, 'Maybe')) # True

print(testpkg.__dict__.keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'fool', 'Fool', 'maybe', 'Maybe', '__all__'])

此时FooBar被“绑定”到了testpkg这个包的命名空间下。

补充个__all__讲解

__all__ 是一个导出控制列表,是一个字符串列表。

当你这样用的时候:

from testpkg import *

Python 会只导入 __all__ 里列出的名字

也就是说:

  • 如果 __init__.py 里有 __all__ = ['Fool', 'Maybe']
  • 那么 from testpkg import * 只会导入 FoolMaybe
  • 其他即使你在 __init__.py 里定义了,也不会随 * 导出。

注意

  • __all__ 只影响 from ... import * 这种用法。
  • 普通的 import testpkg 不受影响。

非常好,你对模块命名空间、以及 __init__.py 的作用已经理解得很清楚了!下面我会在你的基础上,进一步系统性讲解 __init__.py 文件,帮助你更深入理解它的地位与用法。


__init__.py 文件

一、__init__.py 的本质

  • __init__.py包的初始化脚本
  • 只要目录下有 __init__.py,Python 才会把这个目录当做一个(Python 3.3+ 以后支持“隐式命名空间包”,但强烈建议有 __init__.py,便于兼容和控制行为)。
  • 当你 import testpkg 时,Python 实际执行 testpkg/__init__.py,并把里面的内容放进 testpkg 这个模块(包)对象的命名空间。

二、__init__.py 的作用

1. 标识包

  • 没有 __init__.py,Python 2 不认这个目录是包,会报错。
  • Python 3.3+ 的确可以没有(隐式包),但有了__init__.py能更清楚、兼容、可控。

2. 包初始化

  • 你可以在 __init__.py 里写包初始化逻辑,比如设置全局变量、初始化状态、打印调试信息等。

3. 控制包的“顶层接口”(API 设计

  • 你可以在 __init__.py 里导入(或重新命名)子模块、类、函数,让用户用更简单的方式访问包内容。
  • 例如:
    # testpkg/__init__.py
    from .fool import Fool
    from .maybe import Maybe
    
    这样用户可以 from testpkg import Fool,不用知道子模块结构。

4. 控制 from testpkg import * 导出的内容

  • 通过定义 __all__ 列表,决定哪些名字会被 * 导出。
    __all__ = ['Fool', 'Maybe']
    

5. 可以导入子包、子模块

  • 你可以在 __init__.py 里导入子包、子模块,甚至重命名,隐藏实现细节。
    from . import fool as _fool
    

三、实践举例

1. 最简单的情况

# 空文件
  • 只起到“标识包”的作用。

2. 聚合包的接口(对外API)

# testpkg/__init__.py
from .fool import Fool
from .maybe import Maybe
__all__ = ['Fool', 'Maybe']
  • 这样 testpkg.Fooltestpkg.Maybe 就变成包的“顶层接口”。

3. 初始化逻辑

# testpkg/__init__.py
print("testpkg包被导入了!")
_config = {"debug": True}
  • 导入包时会输出一句话,设置包级别的配置。

4. 导出模块而不是类

# testpkg/__init__.py
from . import fool
from . import maybe
__all__ = ['fool', 'maybe']
  • 这样 from testpkg import fool 就可以直接用 testpkg.fool.Fool

四、几点注意

  1. 相对导入和绝对导入

    • from .fool import Fool:点号代表“当前包”,推荐包内部用相对导入。
    • from testpkg.fool import Fool:绝对导入,避免循环依赖出错。
  2. 循环引用问题

    • 包内部如果互相引用,要注意不要在模块顶层出现死循环引用,否则会报 ImportError
  3. __init__.py 并不是必须的,但建议始终写上

    • 这样会让代码更清晰,有更好的兼容性。
  4. 包结构的“封装”思路

    • 你可以只暴露有限的接口给用户(通过__init__.py),隐藏实现细节。

五、你可以这样理解

  • 包(package) = 文件夹 + __init__.py
  • 包的命名空间 = __init__.py 的命名空间
  • 你让包有哪些“顶级名字”,就在 __init__.py 里加

网站公告

今日签到

点亮在社区的每一天
去签到