41. 包

包是一个可以包含其他模块的模块。在 CPython 中,包不是一个单独的对象类别。它仍然是一个模块对象,但它具有导入元数据,告诉导入系统在哪里查找子模块。

在Python级别,这个目录可以是一个包:text app/ __init__.py config.py server.py 您可以将其导入为:python import app import app.config from app.server import run 重要的规则很简单:```text A package is a module with submodule search locations.


## 41.1 包是模块

包对象具有类型`module````python
import email

print(type(email))
print(email.__name__)
```输出:```text
<class 'module'>
email
```就像任何其他模块一样,包也有一个模块字典。```python
import email

print(email.__dict__)
```包命名空间存储普通名称:```text
__name__
__doc__
__package__
__loader__
__spec__
__path__
__file__
__cached__
```它还可以存储函数、类、常量、导入的子模块和重新导出的公共 API 名称。

因此,一个包既是:```text
a namespace object
a container for submodule lookup
```## 41.2 的角色`__init__.py`常规包装通常有一个`__init__.py`文件。```text
project/
    app/
        __init__.py
        config.py
        routes.py
``` CPython 导入时`app`,它执行:```text
app/__init__.py
```代码在`__init__.py`初始化包命名空间。

例子:```python
# app/__init__.py

VERSION = "1.0.0"

def create_app():
    return "app"
```然后:```python
import app

print(app.VERSION)
print(app.create_app())
```该文件`__init__.py`不仅仅是一个标记。它是可执行模块代码。

## 41.3 最小包导入

对于此布局:```text
demo/
    __init__.py
    util.py
```和这段代码:```python
import demo
```CPython大致执行:```text
find package named "demo"
create module object for "demo"
set package metadata
insert "demo" into sys.modules
execute demo/__init__.py
bind name "demo" in caller namespace
```导入后:```python
import sys
import demo

print(sys.modules["demo"] is demo)
```输出:```text
True
```该包缓存在`sys.modules`以其完全限定名称。

## 41.4 导入子模块

对于:```python
import demo.util
```CPython 首先导入父包。

从概念上讲:```text
import demo
then search demo.__path__ for util
then import demo.util
then set demo.util attribute
```导入成功后:```python
import demo.util

print(demo.util)
print(demo.util.__name__)
```输出形状:```text
<module 'demo.util' from '.../demo/util.py'>
demo.util
```子模块单独缓存:```python
import sys
import demo
import demo.util

print(sys.modules["demo"])
print(sys.modules["demo.util"])
print(demo.util is sys.modules["demo.util"])
```父包和子模块是不同的模块对象。

## 41.5 完全限定的模块名称

包创建分层模块名称。```text
app
app.config
app.server
app.server.http
```每个导入的模块都有一个完全限定名称。```python
import app.server.http

print(app.__name__)
print(app.server.__name__)
print(app.server.http.__name__)
```输出:```text
app
app.server
app.server.http
```完全限定名称是使用的关键`sys.modules````python
import sys

print(sys.modules["app"])
print(sys.modules["app.server"])
print(sys.modules["app.server.http"])
```这种基于名字的身份很重要。以两个不同的名称导入同一个文件可以创建两个独立的模块对象。

## 41.6 包搜索位置

一个包有`__path__````python
import app

print(app.__path__)
```对于常规包装,`__path__`通常包含包目录。```text
['/path/to/project/app']
``` CPython 导入时:```python
import app.config
```它搜索`app.__path__`,不是完整的顶级`sys.path`

这种区别是核心的:

|进口|搜索路径|
|---|---|
|`import app` | `sys.path` |
| `import app.config` | `app.__path__` |
| `import app.server.http` | `app.server.__path__`|

包控制其子项的位置。

## 41.7`__spec__`和套餐

每个现代进口模块都有一个`__spec__`

对于包,模块规范包括子模块搜索位置。```python
import app

print(app.__spec__)
print(app.__spec__.name)
print(app.__spec__.origin)
print(app.__spec__.submodule_search_locations)
```对于包来说,这通常是非空的:```python
app.__spec__.submodule_search_locations
```对于普通模块来说,通常是`None`

导入系统使用它来区分模块和包。

## 41.8 包元数据

常规包通常具有以下属性:

|属性 |意义|
|---|---|
|`__name__`|完全限定的包名称 |
|`__package__`|用于相对导入的包上下文 |
|`__path__`|子模块搜索位置 |
|`__spec__`|进口规格|
|`__loader__`|初始化包的加载器 |
|`__file__`|路径至`__init__.py`,如果有文件支持 |
|`__cached__`|缓存字节码的路径(如果可用)|

例子:```python
import json

print(json.__name__)
print(json.__package__)
print(json.__path__)
print(json.__file__)
print(json.__cached__)
```因此,包可以作为普通对象来观察。

## 41.9`__package__`这`__package__`属性控制相对导入分辨率。

里面`app/server.py`:

```python
from .config import Settings
```前导点的含义是:```text
resolve "config" relative to the current package
```对于模块`app.server`,包上下文通常是:```text
app
```所以:```python
from .config import Settings
```决心:```python
from app.config import Settings
```对于包模块,例如`app`, `__package__`通常是`"app"`

对于子模块,例如`app.server`, `__package__`通常是`"app"`

对于嵌套子模块,例如`app.http.server`, `__package__`通常是`"app.http"`

## 41.10 包执行顺序

Given this layout:```text
app/
    __init__.py
    config.py
    server.py
```这个导入:```python
import app.server
```CPython 按以下顺序执行:```text
1. app/__init__.py
2. app/server.py
```如果`server.py`进口`config.py`:

```python
# app/server.py
from . import config
```那么执行就变成:```text
1. app/__init__.py
2. app/server.py starts
3. app/config.py executes
4. app/server.py continues
```父包在子模块之前加载。

## 41.11 子模块的包属性

之后:```python
import app.server
```父包通常接收一个属性:```python
app.server
```该属性指向子模块对象。

等效视图:```python
import sys
import app.server

assert app.server is sys.modules["app.server"]
```这种绑定很重要,因为用户代码经常浏览包属性:```python
import app.server

app.server.run()
```导入系统维护父包和子模块之间的连接。

## 41.12`import package.module`与`from package import module`这两种形式在名称绑定方面相似但不相同。```python
import app.config
```绑定`app`在调用者命名空间中。```python
from app import config
```绑定`config`在调用者命名空间中。

两者都正常加载`app.config`

例子:```python
import app.config

print(app.config)
from app import config

print(config)
```加载的模块对象通常是相同的:```python
import app.config
from app import config

print(app.config is config)
```输出:```text
True
```区别在于导入模块命名空间中的名称

## 41.13 公共包API

包可以通过以下方式公开干净的公共 API`__init__.py`。

布局示例:```text
httpkit/
    __init__.py
    client.py
    response.py
    errors.py
```内部文件:```python
# httpkit/client.py

class Client:
    ...
# httpkit/errors.py

class HTTPKitError(Exception):
    ...
```正面:```python
# httpkit/__init__.py

from .client import Client
from .errors import HTTPKitError

__all__ = ["Client", "HTTPKitError"]
```用户可以写:```python
from httpkit import Client, HTTPKitError
```这使得包作者可以更改内部文件布局同时保留公共导入

## 41.14`__all__`名称`__all__`定义 star 导入使用的公共名称。```python
__all__ = ["Client", "HTTPKitError"]
```为了:```python
from httpkit import *
```Python 导入中列出的名称`httpkit.__all__`。

没有`__all__`, star import 导出不以 开头的名称`_`。`__all__`作为文档也很有用它告诉读者哪些名称打算作为公共包 API

## 41.15 包装外观和导入成本

包装外观改善了人体工程学但会增加导入时间

这很方便:```python
# package/__init__.py

from .database import Database
from .server import Server
from .client import Client
from .analytics import Tracker
```但现在:```python
import package
```加载所有这些模块

如果这些模块导入大量依赖项初始化本机库读取文件或执行配置这可能会很昂贵

较轻的包外观可能只暴露廉价的名称:```python
# package/__init__.py

__version__ = "1.0.0"
```然后用户直接导入重组件:```python
from package.client import Client
```良好的包设计可以平衡便利性启动时间和依赖性清晰度

## 41.16 惰性包属性

包可以使用模块级别延迟公开属性`__getattr__`。```python
# package/__init__.py

def __getattr__(name):
    if name == "Client":
        from .client import Client
        return Client
    raise AttributeError(name)
```然后:```python
import package

Client = package.Client
```进口`.client`仅当`Client`被要求

这可以降低启动成本同时保留良好的公共 API

权衡是复杂性延迟包导出使导入行为不那么明显并且稍后可能会出现错误

## 41.17 常规套餐

常规包装有一个`__init__.py`。```text
pkg/
    __init__.py
    mod.py
```特性:```text
executes __init__.py when imported
has __file__ pointing to __init__.py
has __path__ pointing to package directory
can define package-level API
can contain submodules and subpackages
```大多数应用程序包和库都使用常规包

## 41.18 命名空间包

命名空间包没有单一的`__init__.py`。

它可以从多个目录组装

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

dir2/
    plugins/
        beta.py
```如果两者都`dir1``dir2`正在`sys.path`, 然后`plugins`可以是命名空间包。```python
import plugins.alpha
import plugins.beta
```套餐`plugins`可能有一个`__path__`包含两个位置

当多个发行版贡献于一个包命名空间时命名空间包非常有用

## 41.19 常规包与命名空间包

|特色|普通套餐|命名空间包|
|---|---|---|
|`__init__.py`|是的 |没有 |
|执行包初始化代码 |是的 |没有单个初始化器 |
|可以直接定义包级名称 |是的 |有限公司|
|可以跨多个目录|通常没有 |是的 |
|常用|普通库和应用程序 |插件命名空间拆分发行版 |

常规包提供显式初始化命名空间包提供了灵活的组合

## 41.20 子包

子包是另一个包内的包。```text
app/
    __init__.py
    api/
        __init__.py
        users.py
        posts.py
```您可以导入:```python
import app.api
import app.api.users
```导入系统解决了每个级别。```text
app
app.api
app.api.users
```每个级别都有自己的模块对象和自己的`sys.modules`入口。```python
import sys
import app.api.users

print(sys.modules["app"])
print(sys.modules["app.api"])
print(sys.modules["app.api.users"])
```## 41.21 包中的相对导入

相对导入在包内很常见。```python
from .config import Settings
from .storage import Store
from ..core import errors
```点表示级别

|语法 |意义|
|---|---|
|`from . import x`|从当前包导入同级 |
|`from .x import y`|从当前包中的子模块导入 |
|`from .. import x`|从父包导入 |
|`from ..x import y`|从父包下的兄弟导入 |

相对导入使内部依赖关系独立于顶级包名称

它们还需要正确的包上下文直接运行包文件可能会破坏它们

## 41.22 直接脚本执行问题

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

```python
from .config import Settings
```这有效:```bash
python -m app.main
```这可能会失败:```bash
python app/main.py
```当按文件路径执行时,`main.py`变成`__main__`, 不是`app.main`。该模块可能会丢失相对导入所需的包上下文

对包代码使用模块执行:```bash
python -m app.main
```## 41.23`__main__.py`一个包可以定义`__main__.py`。```text
tool/
    __init__.py
    __main__.py
    cli.py
```然后:```bash
python -m tool
```执行:```text
tool/__main__.py
```例子:```python
# tool/__main__.py

from .cli import main

main()
```这是使包可执行的标准方法

## 41.24 包初始化副作用

因为`__init__.py`在导入期间执行包初始化可能会产生副作用。```python
# package/__init__.py

print("loading package")
connect_to_service()
```然后:```python
import package
```立即执行该工作

这对于以下情况可能会出现问题:```text
startup time
tests
CLI responsiveness
server cold starts
configuration ordering
optional dependencies
import cycles
```尽可能保持包初始化较小

好的`__init__.py`文件通常包含:```text
version constants
cheap re-exports
small compatibility shims
public API declarations
```避免繁重的工作除非导入时初始化是显式包契约的一部分

## 41.25 包依赖关系图

包结构应该反映依赖方向

一个干净的应用程序可能看起来像:```text
app/
    __init__.py
    main.py
    config.py
    domain/
        __init__.py
        users.py
        posts.py
    storage/
        __init__.py
        db.py
    web/
        __init__.py
        routes.py
```良好的依赖方向:```text
main imports web
web imports domain
web imports storage
storage imports domain
domain imports no app-specific infrastructure
```不良的依赖方向:```text
domain imports web
storage imports main
config imports route handlers
__init__.py imports everything
```错误的包依赖关系图通常会产生循环导入

## 41.26 包中的循环导入

循环导入在包中很常见因为模块经常导入同级模块

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

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

class Post:
    ...
```这可能会失败因为`app.users``app.posts`在顶层执行过程中相互需要

可能的修复

移动共享定义:```text
app/
    models.py
    users.py
    posts.py
```使用本地导入:```python
def create_post():
    from .posts import Post
    return Post()
```使用仅类型检查的导入:```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .posts import Post
```结构修复通常是最好的循环导入通常表明模块边界需要调整

## 41.27 包装级再出口和循环进口

再出口过多`__init__.py`可以创建导入周期

例子:```python
# app/__init__.py
from .server import Server
from .config import Config
```然后里面`server.py`:

```python
from app import Config
```这迫使`app.__init__`在仍在导入时完成`server`。

更安全的内部导入是:```python
from .config import Config
```在包内部更喜欢从定义模块导入而不是从包外观导入

包外观主要供外部用户使用

## 41.28 私有模块

Python 使用私有模块的命名约定。```text
package/
    __init__.py
    public.py
    _internal.py
    _compat.py
```前导下划线表示该模块按照惯例是内部的。```python
from package._internal import helper
```这是允许的但包作者可能会更改`_internal`而不保留兼容性

公共 API 应有意识地记录并重新导出

## 41.29`src`布局

许多 Python 项目都使用`src`布局。```text
project/
    pyproject.toml
    src/
        package/
            __init__.py
            core.py
    tests/
        test_core.py
```此布局有助于防止从存储库根目录意外导入

没有`src`,即使安装的包已损坏测试也可能会意外导入本地文件

`src`,必须安装包或者正确配置路径这样更符合用户行为

## 41.30 包数据

包可以包含数据文件。```text
package/
    __init__.py
    templates/
        page.html
    data/
        defaults.json
```不要假设包数据位于普通文件系统目录中包可以从 zip 文件或其他加载器导入

更喜欢`importlib.resources`:

```python
from importlib.resources import files

data = files("package.data").joinpath("defaults.json").read_text()
```这会向导入系统询问资源而不是手动构建路径`__file__`。

## 41.31`__file__`局限性

很多包都有`__file__`。```python
import package

print(package.__file__)
```但健壮的代码不应假定所有模块和包都具有正常的文件路径

一些模块可能是:```text
built in
frozen
loaded from zip files
loaded by custom importers
namespace packages
```对于包资源请尽可能使用导入系统 API而不是直接路径算术

## 41.32 包版本值

包通常定义`__version__`。```python
# package/__init__.py

__version__ = "1.2.3"
```这很简单也很常见

包还可以使用已安装的包元数据:```python
from importlib.metadata import version

__version__ = version("package-name")
```第二种方法避免重复版本字符串但如果包未作为元数据安装则可能会失败

对于库请保持版本处理简单且可预测

## 41.33 公共 API 稳定性

包布局和公共 API 是不同的东西

内部布局:```text
library/
    _client.py
    _transport.py
    _errors.py
```公共API:```python
from library import Client, LibraryError
```该包可以在更改内部结构的同时保留公共 API。```python
# library/__init__.py

from ._client import Client
from ._errors import LibraryError

__all__ = ["Client", "LibraryError"]
```这种分离使维护者可以自由地进行重构

## 41.34 包导入时间

可以测量包导入时间。```bash
python -X importtime -c "import package"
```缓慢的包导入通常来自:```text
large transitive imports
heavy package __init__.py files
runtime configuration loading
native library initialization
network or file-system work
plugin auto-discovery
large type-hint imports at runtime
```改进通常从制作开始`__init__.py`较小

## 41.35 包初始化模式

一个实用的`__init__.py`通常看起来像:```python
"""
Public API for examplekit.
"""

from .client import Client
from .errors import ExampleKitError

__all__ = [
    "Client",
    "ExampleKitError",
]

__version__ = "0.1.0"
```这是可以接受的`client``errors`进口价格便宜

对于较重的模块:```python
__all__ = ["Client", "ExampleKitError", "__version__"]

__version__ = "0.1.0"

def __getattr__(name):
    if name == "Client":
        from .client import Client
        return Client
    if name == "ExampleKitError":
        from .errors import ExampleKitError
        return ExampleKitError
    raise AttributeError(name)
```仅当启动成本证明其复杂性合理时才使用惰性形式

## 41.36 包和 CPython 内部

在CPython级别包导入仍然是模块导入

差异出现在导入元数据中:```text
regular module:
    __spec__.submodule_search_locations = None

package:
    __spec__.submodule_search_locations = [...]
    __path__ = [...]
```导入子模块时导入系统使用父包路径

简化:```python
def import_child(parent, child_name):
    fullname = parent.__name__ + "." + child_name
    path = parent.__path__
    spec = find_spec(fullname, path)
    return load(spec)
```真正的导入系统处理锁定错误命名空间包缓存和加载器协议

## 41.37 包对象和属性查找

包对象使用普通的模块属性查找。```python
import package

package.name
```这在包字典中查找

如果一个包定义了模块级`__getattr__`,可以动态计算缺失的属性。```python
def __getattr__(name):
    ...
```但导入的子模块通常作为属性存储在父包上。```python
import package.submodule

print(package.submodule)
```这就是为什么包命名空间会随着导入的发生而增长

## 41.38 包导入失败

如果包导入失败`__init__.py`,导入系统从中删除失败的模块`sys.modules`在很多情况下

例子:```python
# broken/__init__.py

raise RuntimeError("failed")
```然后:```python
import broken
```引发异常

导入系统必须避免将损坏的模块缓存起来就像它已成功初始化一样

子模块故障可能更加微妙父包可能导入成功而子模块导入失败

## 41.39 重复的包标识

当同一个包可以不同的名称导入时就会出现重复的包标识

路径问题示例:```text
project/
    app/
        __init__.py
        state.py
```一种导入路径:```python
import app.state
```另一条意外路径:```python
import state
```现在同一个文件可能会被加载两次:```text
sys.modules["app.state"]
sys.modules["state"]
```结果:```text
two module global dictionaries
two singleton instances
two registry objects
two class identities
two caches
```这是奇怪错误的常见来源

通过使用一致的绝对包导入并避免不安全来避免这种情况`sys.path`编辑

## 41.40 包和类型标识

类是存储在模块中的对象

如果同一个模块以不同的名称导入两次则其类将创建两次。```python
# app/models.py

class User:
    pass
```如果同时加载:```python
import app.models
import models
```然后:```python
app.models.User is models.User
```或许:```text
False
```从一个类创建的对象可能会失败`isinstance`检查对方

这就是为什么模块标识对于包很重要

## 41.41 包和入口点

安装的包可以通过打包元数据公开控制台脚本

命令行入口点可以调用:```text
package.module:function
```从概念上讲运行命令会导入模块并调用函数

目标示例:```python
# tool/cli.py

def main():
    ...
```入口点:```text
tool = tool.cli:main
```这意味着 CLI 启动成本包括导入`tool.cli`及其依赖项

当启动很重要时请保持 CLI 入口模块较小

## 41.42 插件包

软件包通常支持插件

插件架构可以使用:```text
namespace packages
entry points
importlib
explicit plugin lists
dynamic discovery
```一个简单的显式插件加载器:```python
import importlib

def load_plugin(name):
    return importlib.import_module(f"app_plugins.{name}")
```基于包的插件系统应该避免急切地导入每个插件除非需要

插件导入通常会产生副作用例如注册。```python
# plugin_alpha.py

from registry import register

register("alpha", handler)
```这很有用但导入顺序成为程序行为的一部分

## 41.43 软件包和发行版名称

导入包名称和分发包名称可以不同

例子:```text
distribution name: beautifulsoup4
import name: bs4
```导入系统知道导入名称打包工具知道发行版名称

在读取依赖项列表或包元数据时这种区别很重要。```python
import bs4
```没有说安装的发行版被命名`bs4`。

## 41.44 包和`pyproject.toml`现代 Python 包常用`pyproject.toml`。

项目可以声明:```toml
[project]
name = "examplekit"
version = "0.1.0"
```发行版名称是`examplekit`。

导入包可能是:```text
src/examplekit/
    __init__.py
```对于 CPython 的导入系统只有可导入的包布局和`sys.path`运行时很重要在运行之前安装和打包期间构建元数据很重要

## 41.45 可编辑安装

在开发中包通常以可编辑模式安装。```bash
python -m pip install -e .
```这使得该包可以从工作树导入

从CPython的角度来看结果仍然是基于路径的导入环境已配置以便包源位置显示在导入解析中

可编辑安装可帮助测试和工具像用户一样导入包同时仍然使用实时源文件

## 41.46 包调试清单

当包导入行为异常时请检查:```python
import sys
import package

print(package)
print(package.__name__)
print(getattr(package, "__file__", None))
print(getattr(package, "__path__", None))
print(package.__spec__)
print(sys.modules.get("package"))
```对于子模块:```python
import package.submodule

print(package.submodule)
print(package.submodule.__name__)
print(package.submodule.__file__)
print(sys.modules.get("package.submodule"))
```对于路径问题:```python
import sys

for p in sys.path:
    print(p)
```对于不导入的解析:```python
import importlib.util

print(importlib.util.find_spec("package"))
print(importlib.util.find_spec("package.submodule"))
```## 41.47 最小包导入模型

导入子模块的简化模型:```python
def import_package_child(parent_name, child_name):
    parent = import_module(parent_name)

    fullname = parent_name + "." + child_name

    if fullname in sys.modules:
        return sys.modules[fullname]

    spec = find_spec(fullname, parent.__path__)
    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

    setattr(parent, child_name, module)
    return module
```该模型捕获特定于包的部分:```text
load parent first
search parent.__path__
cache child by fully qualified name
bind child as parent attribute
```## 41.48 良好的包装设计规则

保持包初始化较小

使用`__init__.py`公开稳定的公共 API而不是运行应用程序

更喜欢从定义模块直接内部导入

避免从包外观导入内部模块

避免直接执行包模块的脚本使用`python -m package.module`。

使用`importlib.resources`用于包数据

使用`__all__`记录公共名称

仅当需要拆分包组合时才使用命名空间包

避免修改`sys.path`里面的包代码

保持依赖方向清晰

## 41.49 常见包故障

|症状|可能的原因 |
|---|---|
|相对导入失败 |模块作为脚本而不是 with 运行`-m`|
|部分初始化的包 |循环进口|
|缺少包属性 |子模块尚未导入或外观未导出 |
|两个单例实例 |以两个名称导入同一模块 |
|慢的`import package`|重的`__init__.py`或传递性导入 |
|在存储库中工作但安装后失败 |包装布局错误或缺少包装数据 |
|标准模块阴影 |本地文件或包名冲突 |

## 41.50 要点

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

常规包执行`__init__.py`导入期间

子模块缓存在`sys.modules`在完全限定名称下

一个包的`__path__`控制搜索子模块的位置

相对导入取决于包上下文

命名空间包允许一个包命名空间跨越多个位置

包外观改善了 API 人体工程学但会增加导入成本并产生周期

大多数包错误来自循环导入路径错误重复的模块标识繁重的初始化或直接运行包文件