41. 套餐
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 人体工程学,但会增加导入成本并产生周期。
大多数包错误来自循环导入、路径错误、重复的模块标识、繁重的初始化或直接运行包文件。