40. 模块和导入

模块是 Python 代码加载、命名空间隔离和重用的基本单元。在 CPython 中,模块既是语言级对象,也是导入系统中的运行时记录。

在Python层面,模块是执行后得到的:python import math import os import json 每个导入的名称都绑定到模块对象、包对象、函数、类或其他导出对象。在CPython层面,导入是一个协调的过程,涉及字节码指令、导入钩子、模块规范、加载器、查找器、sys.modules、包路径、文件系统查找、字节码缓存、导入锁和模块执行。

导入系统不是简单的文件包含机制。它是一个运行时协议。

40.1 什么是模块

模块是类型的对象module。```python import sys

print(type(sys)) print(sys.name) 输出:text <class 'module'> sys 模块对象拥有一个字典。该字典是模块的全局命名空间。python import math

print(math.dict["pi"]) print(math.dict["sqrt"])


对于名为`config.py`:

```python
debug = True
port = 8080

def connect():
    return port
```CPython 创建一个模块对象,准备其命名空间,在该命名空间内执行编译后的代码对象,并将生成的绑定保留在`config.__dict__`

从概念上讲:```text
module object
    __dict__
        "__name__"      -> "config"
        "__file__"      -> ".../config.py"
        "__spec__"      -> ModuleSpec(...)
        "debug"         -> True
        "port"          -> 8080
        "connect"       -> function object
```因此,模块是一个可变的命名空间对象。

## 40.2 CPython 中的模块对象

 CPython 中,模块对象是通过`PyModuleObject`类型。

一个简化的心理模型是:```c
typedef struct {
    PyObject_HEAD
    PyObject *md_dict;
    PyObject *md_name;
    PyObject *md_doc;
    PyObject *md_state;
    PyObject *md_weaklist;
    PyModuleDef *md_def;
} PyModuleObject;
```确切的字段可以更改,但重要的一点是稳定的:模块具有关联的字典和可选的 C 级模块定义/状态。

该字典存储普通的 Python 名称。当 Python 代码计算模块内的全局名称时,CPython 通常首先查找该模块字典。

例如:```python
x = 10

def f():
    return x
```功能`f`不复制`x`。它通过其函数对象存储对模块全局字典的引用。什么时候`f`执行`return x`,CPython解析`x`使用该字典的全局查找。

## 40.3 导入即执行

导入 Python 模块会执行其顶级代码。

为了`example.py`:

```python
print("loading example")

value = 42

def get_value():
    return value
```第一次导入执行文件:```python
import example
```输出:```text
loading example
```第二次导入通常不会再次执行该文件:```python
import example
```没有出现输出,因为 CPython 在以下位置找到了现有模块`sys.modules`

这种行为是根本性的。导入有副作用,因为模块顶级代码运行。

好的模块顶级代码通常包含定义和廉价的初始化:```python
CONSTANT = 100

def parse(text):
    ...
```有风险的模块顶级代码执行昂贵的或外部可见的工作:```python
connect_to_database()
delete_old_files()
start_threads()
make_network_request()
```此类代码在导入期间运行,有时在应用程序完全初始化之前运行。

## 40.4 导入声明

声明:```python
import package.module
```并不直接表示“打开此文件”。

这意味着:```text
resolve a module name
find a module specification
create or reuse a module object
initialize import-related attributes
execute the module if needed
bind a name in the caller's namespace
```声明:```python
import os.path
```通常绑定`os`, 不是`os.path`,在本地命名空间中:```python
import os.path

print(os)
print(os.path)
```声明:```python
from os import path
```绑定`path`直接地:```python
from os import path

print(path)
```声明:```python
from math import sqrt
```如果需要导入模块,然后检索`sqrt`来自该模块并将其绑定到调用者的名称空间中。

## 40.5 导入字节码

CPython 将 import 语句编译为字节码。

例子:```python
import math
```编译后的代码使用与导入相关的字节码指令。确切的指令序列因 Python 版本而异,但从概念上讲,它是这样做的:```text
load import machinery
import module named "math"
bind result to name "math"
```为了:```python
from math import sqrt
```字节码从概念上讲是这样做的:```text
import module named "math"
load attribute "sqrt"
bind local/global name "sqrt"
```你可以检查这个`dis`:

```python
import dis

def f():
    import math
    return math.sqrt(9)

dis.dis(f)
```import 语句是正常字节码执行的一部分。没有单独的预处理器步骤。

## 40.6`__import__`在语言级别,导入语句最终通过通过暴露的导入机制进行路由`builtins.__import__`。```python
import builtins

print(builtins.__import__)
```您可以直接调用它:```python
math_module = __import__("math")
print(math_module.sqrt(9))
```但大多数代码不应该调用`__import__`直接地。使用`importlib.import_module`对于动态导入:```python
import importlib

mod = importlib.import_module("math")
print(mod.sqrt(9))
```功能`__import__`存在是因为导入是动态的。 Python 代码可以在运行时通过字符串名称导入模块。

## 40.7`sys.modules`

`sys.modules`是中央模块缓存。

它是一个将完全限定的模块名称映射到模块对象的字典。```python
import sys
import math

print(sys.modules["math"] is math)
```输出:```text
True
```在加载模块之前,导入系统会检查`sys.modules`。

从概念上讲:```python
if fullname in sys.modules:
    return sys.modules[fullname]
else:
    module = load_module(fullname)
    sys.modules[fullname] = module
    return module
```真正的过程更加小心,因为它必须处理包、循环导入、失败的导入、锁和加载器协议。

关键属性仍然存在:导入按模块名称缓存。

## 40.8 为什么要在执行前插入模块

CPython通常会插入一个模块到`sys.modules`在执行其代码之前。

这对于循环导入是必要的。

认为`a.py`包含:```python
import b

x = 1
```和`b.py`包含:```python
import a

y = 2
```什么时候`a`进口`b`, 和`b`进口`a`,导入系统必须避免无限递归。它通过将部分初始化的模块对象放入`sys.modules`。

代价是循环导入可能会观察到不完整的模块。

例子:```python
# a.py
import b

x = 1
# b.py
import a

print(a.x)
```这可能会失败因为`a.x`尚未分配时`b`读它

循环进口并不被禁止但需要小心通常的解决方法是将导入移动到函数内部将共享定义移动到第三个模块中或者避免顶级交叉依赖

## 40.9 模块初始化顺序

Python 源模块的简化导入顺序如下所示:```text
1. Receive module name, such as "pkg.mod".
2. Check sys.modules.
3. Search sys.meta_path for a finder.
4. Finder returns a ModuleSpec.
5. Import machinery creates a module object.
6. Module is inserted into sys.modules.
7. Loader executes module code.
8. Import machinery returns the module object.
9. Import statement binds names in caller namespace.
```对于源文件执行意味着:```text
read source
decode source
compile source to code object
execute code object in module namespace
```对于扩展模块来说执行意味着调用本机初始化代码

对于内置模块执行时使用编译到 CPython 中的内置初始化逻辑

## 40.10`ModuleSpec`现代 Python 导入用途`ModuleSpec`对象来描述如何加载模块。

模块规范包含以下信息:```text
module name
loader
origin
package search locations
cached bytecode path
whether the module is a package
```您可以检查模块的规格:```python
import json

print(json.__spec__)
print(json.__spec__.name)
print(json.__spec__.origin)
print(json.__spec__.loader)
print(json.__spec__.submodule_search_locations)
```对于普通模块来说,`submodule_search_locations`通常是`None`。

对于包它包含可以找到子模块的路径

## 40.11 查找器和加载器

导入系统将查找与加载分开

发现者回答:```text
Can this module name be found?
If yes, what spec describes it?
```装载机回答:```text
How should this module be created and executed?
```这种分离允许 Python 从许多地方导入:```text
source files
bytecode files
built-in modules
extension modules
zip archives
namespace packages
custom import hooks
memory-backed module stores
remote systems, if a custom importer implements it
```标准导入系统是可扩展的因为它是基于协议的

## 40.12`sys.meta_path`

`sys.meta_path`是导入系统中的第一个主要挂钩点

它是查找器对象的列表每个查找器都可以决定它是否知道如何处理模块名称。```python
import sys

for finder in sys.meta_path:
    print(finder)
```简化的导入搜索可以执行以下操作:```python
for finder in sys.meta_path:
    spec = finder.find_spec(fullname, path, target)
    if spec is not None:
        return spec
```典型的条目句柄:```text
built-in modules
frozen modules
path-based modules
```基于路径的查找器负责搜索目录和其他路径条目

## 40.13`sys.path`

`sys.path`是顶级模块的导入搜索位置列表。```python
import sys

for entry in sys.path:
    print(entry)
```当你写:```python
import mymodule
````mymodule`不是内置的或冻结的基于路径的导入系统在以下位置搜索条目`sys.path`。

条目通常是:```text
directory of the running script
current working directory in interactive mode
standard library directories
site-packages directories
paths from PYTHONPATH
virtual environment paths
zip archives
```这就是为什么要改变`sys.path`改变导入行为。```python
import sys

sys.path.insert(0, "/custom/modules")

import mymodule
```这在受控工具中很有用但也可能会产生脆弱的导入行为

## 40.14 套餐

包是可以包含子模块的模块

从历史上看目录通过包含`__init__.py`文件:```text
pkg/
    __init__.py
    parser.py
    lexer.py
```然后:```python
import pkg.parser
```负载`pkg`首先然后`pkg.parser`。

该文件`pkg/__init__.py`导入包时执行

例如:```python
# pkg/__init__.py
print("loading package")
version = "1.0"
import pkg
print(pkg.version)
```包仍然是一个模块对象不同之处在于它有包搜索位置

## 40.15 包属性

包通常定义与导入相关的属性:```text
__name__
__package__
__path__
__spec__
__file__
__cached__
```重要的特定于包的属性是`__path__`。```python
import package

print(package.__path__)

__path__告诉导入系统在该包内的哪里搜索子模块。

为了:python import package.submodule 导入系统搜索package.__path__,不是顶级的sys.path

40.16 命名空间包

Python 支持命名空间包。这些是没有单个的包__init__.py文件。

命名空间包可以分布在多个目录中。

例子:```text dir1/ plugins/ alpha.py

dir2/ plugins/ beta.py ```如果两者都dir1dir2正在sys.path,Python可以处理plugins作为命名空间包。

那么两者都可以工作:```python import plugins.alpha import plugins.beta


它们还使导入解析更加复杂,因为一个包可能有多个搜索位置。

## 40.17 绝对导入

绝对导入从顶级导入命名空间开始。```python
import package.module
from package import module
```在包内,这仍然搜索名为的顶级包`package`

在引用外部或顶级模块时,为了清晰起见,首选绝对导入。

例子:```python
from project.config import Settings
```这告诉读者导入的名称来自哪里。

## 40.18 相对进口

相对导入是针对当前包解析的。```python
from . import parser
from .lexer import tokenize
from ..config import Settings
```相对进口取决于`__package__`

它们仅在模块作为包的一部分执行时才起作用。这就是为什么直接将包模块作为脚本运行会破坏相对导入的原因。

例如:```text
project/
    app/
        __init__.py
        main.py
        config.py
```里面`main.py`:

```python
from .config import Settings
```这在执行时有效:```bash
python -m app.main
```执行时可能会失败:```bash
python app/main.py
```因为直接脚本执行会改变模块的命名和打包方式。

## 40.19`__main__`作为程序入口点执行的模块被命名为`__main__`。```python
print(__name__)
```当作为脚本运行时:```bash
python script.py
```输出:```text
__main__
```导入时:```python
import script
```模块名称是:```text
script
```这就是为什么 Python 程序通常使用:```python
def main():
    ...

if __name__ == "__main__":
    main()
```该防护措施可防止程序入口代码在导入期间运行。

## 40.20 运行模块`-m`命令:```bash
python -m package.module
```通过导入名称而不是文件路径运行模块。

这很重要,因为模块获得了正确的包上下文。

对于包代码,首选:```bash
python -m package.module
```超过:```bash
python package/module.py
````-m`形式允许相对导入工作,因为 CPython 知道模块的包。

## 40.21 字节码缓存文件

CPython 可以将编译后的字节码缓存在`__pycache__`

例子:```text
package/
    module.py
    __pycache__/
        module.cpython-312.pyc
```当缓存有效时,字节码缓存可以避免每次导入时重新编译源代码。

一个`.pyc`文件包含:```text
magic number
cache metadata
marshaled code object
```幻数标识字节码格式版本。当 CPython 不兼容地更改字节码时,它会发生变化。

CPython 使用基于时间戳或基于哈希的失效来检查缓存有效性,具体取决于文件的生成方式。

## 40.22 源模块和代码对象

对于一个正常的`.py`模块时,加载器将源代码编译为代码对象。

然后它执行模块字典中的代码对象。

从概念上讲:```python
module = types.ModuleType("example")
code = compile(source_text, filename, "exec")
exec(code, module.__dict__)
```尽管实际的导入系统处理更多细节,但这与真实模型很接近。

重要的一点是,模块执行是普通的代码执行,以模块字典作为全局命名空间。

## 40.23 内置模块

内置模块被编译成 CPython 可执行文件或链接运行时。

例子通常包括:```python
import sys
import builtins
import time
```内置模块的可用性取决于平台和构建配置。

内置模块不需要定位`.py`文件。它的加载程序根据 C 级定义对其进行初始化。

您可以检查内置模块名称:```python
import sys

print(sys.builtin_module_names)
```内置模块提供了完整的基于文件的导入系统可用之前所需的核心运行时服务。

## 40.24 冻结模块

冻结模块是作为冻结字节码或等效静态数据嵌入到 CPython 二进制文件中的 Python 模块。

它们帮助在文件系统导入系统完全运行之前引导导入机器。

这会产生引导问题:```text
importlib implements imports
but importlib itself must be imported
so parts of importlib are frozen
```冻结模块通过使选定的 Python 代码可用而不需要普通的源文件导入来解决这个循环。

## 40.25 扩展模块

扩展模块是加载到 CPython 中的本机共享库。

在类 Unix 系统上,这些通常是`.so`文件。在 Windows 上,它们通常是`.pyd`文件。

导入示例:```python
import _sqlite3
import _ssl
import _hashlib
```扩展模块提供了由 CPython 调用的初始化函数。现代扩展模块尽可能使用多阶段初始化。

扩展模块必须遵循 CPython  C API 规则:```text
create module object
define methods
manage reference ownership
set exceptions on failure
return initialized module
```由于扩展模块在 Python 进程内运行本机代码,因此错误可能会使解释器崩溃。

## 40.26 单相和多相初始化

较旧的扩展模块通常使用单相初始化。初始化函数一步创建并返回一个模块对象。

现代扩展模块可以使用多阶段初始化。在该模型中,模块创建和模块执行是分开的。

这更好地匹配 Python 级别的导入语义,并更清晰地支持每个模块的状态。

多阶段初始化对于以下方面很重要:```text
subinterpreter compatibility
module reloading behavior
cleaner module state
avoiding process-global mutable state
future isolation improvements
```将所有状态存储在全局 C 变量中的 C 扩展可能在简单情况下工作,但在子解释器或重复初始化时可能表现不佳。

## 40.27 导入锁

导入需要锁定。

如果没有锁定,两个线程可以尝试同时导入和初始化同一模块。

导入系统使用锁来确保模块不会以不安全的方式被多个线程并发执行。

当模块初始化有副作用时,这一点尤其重要:```python
# database.py
connection_pool = create_pool()
```如果两个线程在没有锁定的情况下同时导入此模块,它们可能会创建重复的全局状态或观察到部分初始化。

导入锁阻止了许多此类竞争。

## 40.28 重新加载模块

Python 可以使用以下命令重新加载模块`importlib.reload````python
import importlib
import config

importlib.reload(config)
```重新加载使用现有模块对象重新执行模块代码。

这会产生微妙的后果。

认为`config.py`原来包含:```python
value = 1
```编辑后为:```python
value = 2
```重新加载更新`config.value`

但其他地方的现有引用可能仍然指向旧对象。```python
from config import value

import config
import importlib

importlib.reload(config)

print(value)        # old binding
print(config.value) # new module attribute
```重新加载对于开发工具、笔记本和插件系统很有用,但它不是完整的进程重置。

## 40.29 导入副作用

导入副作用通常是令人困惑的行为的根源。

该模块有一个明显的副作用:```python
# noisy.py
print("imported noisy")
```该模块有一个隐藏的副作用:```python
# registry.py
handlers = {}

def register(name, fn):
    handlers[name] = fn
# plugin.py
from registry import register

def handle(x):
    return x

register("plugin", handle)
```输入`plugin`变异`registry.handlers`。

这种模式在插件系统ORM测试框架和 Web 框架中很常见它可能很有用但这意味着导入顺序成为程序行为的一部分

## 40.30 惰性导入

延迟导入会延迟导入模块直到需要时才导入

例子:```python
def parse_json(text):
    import json
    return json.loads(text)
```这可以减少启动时间或避免未使用的代码路径期间的可选依赖项

但惰性导入需要权衡:```text
errors appear later
first call may become slower
dependency structure becomes less visible
circular imports may be hidden rather than fixed
```当有意使用时延迟导入非常有用它们不应该成为不良模块结构的默认解决方法

## 40.31 可选导入

可选导入对于特征检测很常见。```python
try:
    import uvloop
except ImportError:
    uvloop = None
```小心广泛的异常处理这常常是错误的:```python
try:
    import plugin
except Exception:
    plugin = None
```它隐藏了真正的错误`plugin`。

更喜欢捕捉`ImportError`或者`ModuleNotFoundError`缩小范围并在需要时验证哪个模块出现故障。```python
try:
    import optional_backend
except ModuleNotFoundError as exc:
    if exc.name != "optional_backend":
        raise
    optional_backend = None
```这可以避免隐藏丢失的传递依赖项

## 40.32 导入名称绑定

不同的导入形式绑定不同的名称

|声明|绑定名称 |
|---|---|
|`import os` | `os` |
| `import os.path` | `os` |
| `import os.path as p` | `p` |
| `from os import path` | `path` |
| `from math import sqrt as s` | `s` |
| `from module import *`|许多名字|

导入系统加载模块然后 import 语句绑定当前名称空间中的名称

这些是相关但独立的操作

## 40.33 明星进口

星形导入将导出的名称复制到当前命名空间中。```python
from module import *
```如果模块定义`__all__`,Python 导入这些名称。```python
__all__ = ["connect", "close"]
```没有`__all__`, Python 导入不以下划线开头的名称

在交互式会话和包外观模块之外通常不鼓励星型导入因为它们掩盖了名称的来源

受控的包外观可以小心地使用它们:```python
# package/__init__.py
from .client import Client
from .errors import PackageError

__all__ = ["Client", "PackageError"]
```## 40.34 包外观

包可以从子模块重新导出名称。```python
# library/__init__.py
from .client import Client
from .config import Config

__all__ = ["Client", "Config"]
```然后用户可以写:```python
from library import Client
```而不是:```python
from library.client import Client
```这改进了 API 人体工程学设计但会增加导入成本如果`library.__init__`导入许多重子模块然后`import library`变得昂贵

好的封装外观可以平衡便利性和启动成本

## 40.35 导入性能

导入时间对于命令行工具服务器冷启动测试和短时间运行的脚本很重要

进口成本来自:```text
file-system searches
source decoding
bytecode validation
compilation if cache is missing
module execution
transitive imports
native extension loading
top-level initialization
```您可以通过以下方式检查导入时间:```bash
python -X importtime -c "import your_package"
```这将打印导入时间树

提高导入性能的常见方法:```text
avoid heavy top-level work
delay optional imports
reduce large dependency chains
avoid importing test-only modules at runtime
keep package __init__.py small
avoid broad convenience imports in hot paths
```## 40.36 导入错误

常见的与导入相关的例外情况有

|例外|意义|
|---|---|
|`ModuleNotFoundError`|找不到请求的模块 |
|`ImportError`|导入失败有更广泛的原因 |
|`AttributeError`|模块已加载但请求的属性不存在 |
|`SyntaxError`|无法编译源模块 |
|本机加载错误 |扩展模块加载失败 |

例子:```python
from package import missing_name
```如果`package`存在但是`missing_name`Python 可能会引发`ImportError`。

例子:```python
import missing_package
```通常会提高:```text
ModuleNotFoundError
```进口故障诊断应区分:```text
the target module is missing
a transitive dependency is missing
the module exists but raised during execution
the requested exported name is missing
a native extension failed to load
```## 40.37 Circular Imports

当模块在顶层执行期间相互依赖时就会发生循环导入

例子:```python
# users.py
from posts import Post

class User:
    ...
# posts.py
from users import User

class Post:
    ...
```这可能会失败因为每个模块在完成初始化之前都需要另一个模块

常见修复

1. Move shared types into a third module.```text
models/
    base.py
    users.py
    posts.py
```2. 对仅运行时依赖项使用本地导入。```python
def create_post():
    from posts import Post
    return Post()
```3. 使用延迟类型注释。```python
from __future__ import annotations

class User:
    posts: list[Post]
```4.依赖接口而不是具体模块

最好的解决办法通常是结构性的循环导入通常表明模块边界选择不当

## 40.38 导入和类型检查

如果注释导入运行时对象类型提示可能会创建导入循环

一个常见的模式是:```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from posts import Post

class User:
    def add_post(self, post: "Post") -> None:
        ...

TYPE_CHECKING在运行时为 false,因此导入对类型检查器可见,但在执行期间会被跳过。

这可以减少运行时导入周期,同时保留类型信息。

现代 Python 还支持在多种上下文中推迟注释评估,这进一步减少了键入的运行时导入。

40.39 导入挂钩

导入挂钩让程序可以自定义导入行为。

可以将自定义查找器放置在sys.meta_path

自定义加载程序可以从非标准源创建和执行模块。

用例包括:text zip import plugin systems test isolation sandboxed module loading import tracing encrypted module stores remote module stores generated modules 最小的取景器骨架如下所示:```python class Finder: def find_spec(self, fullname, path=None, target=None): if fullname == "virtual_module": ... return None


导入钩子功能强大。它们影响全局程序行为,因此它们应该是狭窄的且可预测的。

## 40.40`importlib`

`importlib`是导入系统的标准库接口。

常用操作:```python
import importlib

mod = importlib.import_module("json")
mod = importlib.reload(mod)
```有用的低级部分包括:```text
importlib.util.find_spec
importlib.util.module_from_spec
spec.loader.exec_module
importlib.machinery.PathFinder
importlib.machinery.SourceFileLoader
```手动加载可以如下所示:```python
import importlib.util

spec = importlib.util.spec_from_file_location("custom_name", "/path/to/file.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
```这会从特定文件创建并执行模块。

对于普通应用程序代码,更喜欢普通导入。仅在构建工具、插件系统、加载器或运行时模块系统时才使用手动 importlib 加载。

## 40.41 模块标识

模块标识基于中的密钥`sys.modules`

如果以两个不同的名称导入同一文件,CPython 可以创建两个不同的模块对象。

问题示例:```text
project/
    package/
        __init__.py
        settings.py
```如果程序的一部分导入:```python
import package.settings
```另一个路径操作会导致:```python
import settings
```那么同一个文件可能会以不同的名称加载两次。

这可以复制模块全局变量:```text
two registries
two singleton objects
two class identities
two caches
```这是直接原因之一`sys.path`操纵可能是危险的。

## 40.42 模块全局变量是共享状态

模块级变量由导入该模块的所有代码共享。```python
# state.py
count = 0

def increment():
    global count
    count += 1
    return count
```每个导入器都会观察相同的模块对象:```python
import state

state.increment()
state.increment()
```这对于常量、注册表、缓存和单例很有用。它还可能使测试变得更加困难,因为状态在导入过程中持续存在。

测试可能需要显式重置模块状态:```python
import state

def test_increment():
    state.count = 0
    assert state.increment() == 1
```或者通过避免可变模块全局变量来隔离行为。

## 40.43 导入时配置

模块通常在导入时读取配置:```python
import os

DEBUG = os.environ.get("DEBUG") == "1"
```这使得配置在导入时固定。如果以后环境发生变化,`DEBUG`不会自动更新。

更灵活的设计在需要时读取配置:```python
import os

def debug_enabled():
    return os.environ.get("DEBUG") == "1"
```或集中配置加载:```python
class Settings:
    def __init__(self):
        self.debug = os.environ.get("DEBUG") == "1"

settings = Settings()
```导入时配置很简单,但它可能会让测试和长时间运行的程序感到惊讶。

## 40.44 模块级`__getattr__`模块可以定义`__getattr__`自定义缺失名称的属性访问。```python
# package/__init__.py

def __getattr__(name):
    if name == "heavy":
        from . import heavy
        return heavy
    raise AttributeError(name)
```这可以实现延迟导出。

然后:```python
import package

package.heavy
```仅在需要时导入重型子模块。

模块级`__getattr__`对于兼容性垫片、弃用和延迟加载很有用。它应该保持简单,因为它改变了正常的属性访问行为。

## 40.45 模块级`__dir__`模块还可以定义`__dir__`。```python
def __dir__():
    return ["Client", "Config", "connect"]
```这控制出现在:```python
dir(module)
```它主要对具有动态属性的模块有用。

## 40.46 启动时导入系统

 CPython 启动期间,必须仔细初始化导入系统本身。

运行时需要足够的导入机制来加载标准库,但大部分导入系统是用 Python 编写的。

引导程序序列使用内置和冻结模块来启动`importlib`

从概念上讲:```text
initialize runtime
initialize builtins and sys
initialize frozen importlib bootstrap code
configure import machinery
initialize sys.path
load site if enabled
start executing user code
```这就是为什么启动内部比普通运行时导入受到更多限制。

## 40.47`site`和环境设置

初始化核心导入机制后,CPython 通常会导入`site`模块除非禁用`-S`

`site`模块配置其他导入路径,包括站点包目录。

它还可能处理:```text
.pth files
user site-packages
virtual environment path adjustments
sitecustomize
usercustomize
```这意味着应用程序启动时的导入环境取决于解释器标志、虚拟环境、安装布局和环境变量。

## 40.48 虚拟环境和导入

虚拟环境改变了 Python 查找已安装包的位置。

它通常会改变:```text
sys.prefix
sys.exec_prefix
site-packages paths
script entry points
```解释器二进制文件可以共享或复制,但导入环境指向虚拟环境的包目录。

这就是为什么:```bash
python -m pip install requests
```在虚拟环境中使得`requests`只能在该环境中导入。

导入系统本身是相同的。搜索路径不同。

## 40.49 从 Zip 文件导入

如果存档打开,Python 可以从 zip 存档导入模块`sys.path`

例子:```bash
python app.zip
```或者:```python
import sys

sys.path.insert(0, "modules.zip")
import mymodule
```Zip 导入使用知道如何在存档中查找模块文件的导入器。

这说明了为什么导入是基于路径条目而不是仅基于目录。一个`sys.path`条目可以由自定义路径挂钩处理。

## 40.50 路径挂钩和路径导入器

对于基于路径的导入,CPython 使用路径钩子来转`sys.path`进入导入器对象。

从概念上讲:```text
sys.path entry
    
sys.path_hooks
    
path importer
    
find module spec
```缓存`sys.path_importer_cache`存储路径条目的导入器对象。```python
import sys

print(sys.path_hooks)
print(sys.path_importer_cache)
```这可以避免重复重建导入器对象。

## 40.51 进口安全问题

导入搜索路径。这使得路径顺序对安全敏感。

如果当前目录出现在标准库之前,则本地文件可以隐藏标准模块。

例子:```text
project/
    json.py
```然后:```python
import json
```可以导入本地的`json.py`而不是标准库`json`

这可能会导致错误或安全问题。

防守做法:```text
avoid naming files after standard library modules
avoid unsafe sys.path insertion
run applications from expected working directories
use virtual environments
inspect module.__file__ when debugging
prefer python -m package.module for package code
```要检查导入的内容:```python
import json

print(json.__file__)
```## 40.52 导入和测试

测试通常强调导入行为。

常见的测试问题包括:```text
tests depend on working directory
local files shadow installed packages
package imported twice under different names
module global state leaks between tests
environment variables read at import time
plugins register themselves during import
```强大的测试设置以与用户相同的方式导入包。

更喜欢测试已安装的软件包行为:```bash
python -m pytest
```来自干净的环境,而不是依赖于偶然的路径布局。

## 40.53 导入和应用程序设计

良好的 Python 应用程序结构可降低导入复杂性。

常见的布局:```text
project/
    pyproject.toml
    src/
        app/
            __init__.py
            main.py
            config.py
            service.py
            storage.py
    tests/
        test_service.py
````src`布局有助于捕获从存储库根目录的意外导入。

一个干净的模块依赖关系图指向内部:```text
main
    depends on service
service
    depends on storage and config
storage
    depends on database driver
config
    depends on environment parsing
```避免低级模块导入高级应用程序入口点的设计。

## 40.54 导入和公共API设计

包的导入表面是其公共 API 的一部分。

例如:```python
from library import Client
```如果有记录,则属于公共合同。

如果包外观保留公共导入,则更改内部模块位置不应破坏用户:```python
# library/__init__.py
from ._client import Client

__all__ = ["Client"]
```私有模块通常使用前导下划线:```text
library/
    __init__.py
    _client.py
    _protocol.py
    public.py
```这是一个约定,而不是访问限制。

## 40.55 导入调试清单

当导入行为令人困惑时,请检查这些值:```python
import sys
import module

print(module)
print(module.__name__)
print(getattr(module, "__file__", None))
print(getattr(module, "__spec__", None))
print(getattr(module, "__package__", None))
print(sys.path)
```对于包裹问题:```python
import package

print(package.__path__)
print(package.__spec__.submodule_search_locations)
```对于缓存问题:```python
import sys

print(sys.modules.get("module_name"))
```对于计时:```bash
python -X importtime -c "import module_name"
```对于直接解析:```python
import importlib.util

print(importlib.util.find_spec("module_name"))
```## 40.56 最小导入算法

简化的导入函数可以写为:```python
def import_module(fullname):
    if fullname in sys.modules:
        return sys.modules[fullname]

    spec = find_spec(fullname)
    if spec is None:
        raise ModuleNotFoundError(fullname)

    module = module_from_spec(spec)
    sys.modules[fullname] = module

    try:
        spec.loader.exec_module(module)
    except Exception:
        del sys.modules[fullname]
        raise

    return module
```真正的 CPython 导入系统更复杂,但这个框架捕获了中心流程:```text
cache lookup
spec discovery
module creation
cache insertion
module execution
error cleanup
return module
```## 40.57 常见故障:模块部分初始化

一个常见的错误如下所示:```text
AttributeError: partially initialized module 'x' has no attribute 'y'
```这通常意味着循环导入或模块阴影问题。

循环导入示例:```python
# a.py
import b

class A:
    ...
# b.py
import a

class B(a.A):
    ...
```什么时候`b``a.A`, 模块`a`存在于`sys.modules`,但它的类`A`has not yet been defined.

修复方法通常是重组模块以便类定义在顶层执行期间不需要相互导入

## 40.58 Common Failure: Shadowing

如果文件以标准库模块命名导入可能会解析为错误的文件

例子:```text
random.py
```里面:```python
import random
```这可能会导入自身而不是标准库`random`。

症状包括:```text
partially initialized module
missing expected attributes
recursive import behavior
strange module.__file__
```查看:```python
import random
print(random.__file__)
```如果需要重命名本地文件并删除过时的缓存文件

## 40.59常见故障:直接运行包文件

鉴于:```text
app/
    __init__.py
    main.py
    config.py
```里面`main.py`:

```python
from .config import Settings
```这可能会失败:```bash
python app/main.py
```因为`main.py`执行为`__main__`,不作为`app.main`。

使用:```bash
python -m app.main
```从包含的目录`app`。

这保留了包上下文并使相对导入起作用

## 40.60 要点

模块是具有命名空间字典的运行时对象

导入模块时每个模块名称都会执行一次其顶级代码`sys.modules`。

导入系统是围绕查找器加载器模块规格路径挂钩和缓存构建的

包是具有子模块搜索位置的模块

循环导入会暴露部分初始化的模块因为 CPython 将模块插入到`sys.modules`执行前

导入系统可通过编程`importlib`, `sys.meta_path`、路径钩子和加载器

大多数导入问题来自循环依赖路径阴影包文件的直接执行导入时副作用或重复的模块标识