Python中的不透明类型
摘要
文章解释了在Python中使用typing.NewType来隐藏内部实现细节的不透明数据类型模式,并通过ShippingOptions示例展示了如何提供最小化的公共接口。
暂无内容
查看缓存全文
缓存时间: 2026/05/26 15:55
# Python 中的不透明类型
来源:https://blog.glyph.im/2026/05/opaque-types-in-python.html
假设你正在编写一个 Python 库。
在这个库中,你有一些状态集合,代表一系列操作的"选项"或"配置"。这样一组选项是一个可能变得越来越复杂的捆绑包。因此,你需要它拥有极其最小化的兼容性表面,并精心选择公共接口,要么很小,要么甚至什么都没有。这样的对象传递状态,并可能包含一些私有行为,但你希望用户只能以非常受限、特定的方式构建它,然后将其作为参数传递给你的 API。
举个例子,想象你正在封装一个处理实体包裹运输的库。
运输包裹的方式有无数种。有不同的承运商可以为你运送。有空运、陆运、海运。有隔夜快递。有需要签收的选项。还有包裹跟踪和挂号信。总之,东西很多。
如果你刚开始实现这样的库,你可能需要一个名为 `ShippingOptions` 的对象来封装其中一些内容。在你的库核心,你可能有一个这样的函数:
```
async def shipPackage(
how: ShippingOptions,
where: Address,
) -> ShippingStatus:
...
```
如果你 *刚开始* 实现这样的库,你知道你对 `ShippingOptions` 的初始实现会是错的;或者,至少不能说"错",但"不完整"。在你对问题领域有足够深入的理解之前,你不应该承诺一个拥有大量不同属性的庞杂公共 API。
然而,`ShippingOptions` 对你库的其余部分至关重要。你需要构造它并传递给各种方法,如 `estimateShippingCost` 和 `shipPackage`。因此,你肯定不希望在你把它变得复杂的过程中出现大量的复杂性和变动。
更糟糕的是,这个对象必须持有大量状态。它有一些属性,甚至可能是相当复杂的内部属性,与不同的运输服务相关。
现在,*今天*,你需要添加 *某些东西*,以便你拥有"不急"、"标准"和"加急"选项。你不能无限期地推迟实现,直到想出一个完美的形状。怎么办?
你需要的工具是 *不透明数据类型* 设计模式。C 语言里充斥着这样的东西(`FILE`、`pthread_*_t`、`fd_set` 等)。头文件中的 `typedef` 很容易实现这一点。
但在 Python 中,如果你暴露一个 `dataclass`——实际上 *任何* 类——即使你保持所有字段私有,*构造函数* 本质上依然是公开的。你可以让它抛出异常之类的,但你的类型检查器仍然不会帮到你的用户;它看起来仍然像一个普通类。
幸运的是,Python 类型提示提供了一个工具:`typing.NewType` (https://docs.python.org/3.14/library/typing.html#newtype)。
让我们回顾一下需求:
1. 我们需要一个类型,客户端代码可以在其类型注解中使用;它需要是公开的。
2. 用户需要能够 *以某种方式* 构造它,即使他们不应该看到它的属性或其内部构造函数参数。
3. 表达高层次的概念(比如"快速运输"),这些概念在未来当我们添加更细致、更复杂的配置时(比如"用支持签名验证的最低成本承运商提供的最快选项运输")仍应得到支持。
为了分别解决这些问题,我们将使用:
1. 一个 *公开的 `NewType`*,它提供我们的公开名称……
2. 它包装一个 *私有类*,该类具有完全私有的属性,以给我们一个实际的数据结构,同时不暴露构造函数,
3. 一组公开的构造函数,返回我们的 `NewType`。
将它们组合在一起,看起来像这样:
```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
```
```
from dataclasses import dataclass
from typing import Literal, NewType
@dataclass
class _RealShipOpts:
_speed: Literal["fast", "normal", "slow"]
ShippingOptions = NewType("ShippingOptions", _RealShipOpts)
def shipFast() -> ShippingOptions:
return ShippingOptions(_RealShipOpts("fast"))
def shipNormal() -> ShippingOptions:
return ShippingOptions(_RealShipOpts("normal"))
def shipSlow() -> ShippingOptions:
return ShippingOptions(_RealShipOpts("slow"))
```
从时间快照来看,这并不那么有趣;我们本可以直接将 `_RealShipOpts` 作为公共类暴露,从而节省一些时间。它暴露一个接受字符串的构造函数在当前时刻并不是什么大问题。对于一个初步的快速实现,我们可以在运输和估算代码中做类似 `if options._speed == "fast"` 这样的检查。
然而,我们在这里做的主要事情是保持灵活性,以便将来能够演化相关的 API,所以让我们看看如何做到这一点。例如,让运输选项包含具体的承运商和货运方式:
```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
```
```
from dataclasses import dataclass
from enum import Enum, auto
from typing import NewType
class Carrier(Enum):
FedEx = auto()
USPS = auto()
DHL = auto()
UPS = auto()
class Conveyance(Enum):
air = auto()
truck = auto()
train = auto()
@dataclass
class _RealShipOpts:
_carrier: Carrier
_freight: Conveyance
ShippingOptions = NewType("ShippingOptions", _RealShipOpts)
def shipFast() -> ShippingOptions:
return ShippingOptions(_RealShipOpts(Carrier.FedEx, Conveyance.air))
def shipNormal() -> ShippingOptions:
return ShippingOptions(_RealShipOpts(Carrier.UPS, Conveyance.truck))
def shipSlow() -> ShippingOptions:
return ShippingOptions(_RealShipOpts(Carrier.USPS, Conveyance.train))
def shippingDetailed(
carrier: Carrier, conveyance: Conveyance
) -> ShippingOptions:
return ShippingOptions(_RealShipOpts(carrier, conveyance))
```
作为一个 `NewType`,我们的公开 `ShippingOptions` 类型没有构造函数。由于 `_RealShipOpts` 是私有的,并且它的所有属性都是私有的,我们可以完全移除旧版本。
任何 *在* 我们的运输库 *内部* 的代码仍然可以访问 `ShippingOptions` 上的私有变量;作为一个 `NewType`,它在运行时与它的基础类型是同一类型,因此它带来极小的开销¹ (https://blog.glyph.im/2026/05/opaque-types-in-python.html#fn:1:opaque-types-in-python-2026-5)。
*在* 我们运输库 *外部* 的客户端仍然可以调用我们所有的公共构造函数:`shipFast`、`shipNormal` 和 `shipSlow` 都以(就调用代码所知)相同的签名和行为工作。
如果你需要在公共 API 中构建和传递某些状态,同时避免与兼容性变动相关的破坏,希望这个技术能对你有所帮助!
---
## 致谢
感谢阅读,也感谢我的赞助人 (https://blog.glyph.im/pages/patrons.html) 支持我在这篇博客上的写作。如果你喜欢这里读到的内容,并且想阅读更多,或者你想支持我的各种开源项目 (https://github.com/glyph/),你可以作为赞助者支持我的工作 (https://blog.glyph.im/pages/patrons.html)。
相似文章
Data types à la carte (2008)
本文提出了一种从独立组件组合数据类型和函数的技术,并将该方法扩展到结合自由单子,从而实现了对Haskell的IO单子的模块化结构。
PyCon US 2026 类型峰会回顾
本文回顾了 PyCon US 2026 类型峰会,详细介绍了关于 Python 类型化进展的关键演讲,包括 PEP 提案、AI 辅助类型检查实验以及类型委员会问答环节。
纵深防御:Python供应链安全实用指南
一份关于通过分层防御保护Python供应链的实用指南,包括使用Ruff进行代码检查、使用哈希锁定依赖项、使用pip-audit进行漏洞扫描、生成SBOM以及使用OIDC证明的可信发布。
静态类型与铲子(2026)
作者认为,2010年代静态类型编程的复兴归功于改进的类型系统(例如 TypeScript、Haskell、Rust),这些系统提供了可空类型处理、和类型以及类型推断,与 Java 和 C++98 等早期语言中糟糕的静态类型形成对比。
现在需要运行五个 Python 类型检查器吗?
一篇博文主张 Python 库维护者应优先在测试套件中运行多个类型检查器,以确保公共 API 的兼容性,并强调了兼容性和代码污染方面的挑战。