首页
/ CPython C API 类型提示对象详解:Py_GenericAlias 与 Py_GenericAliasType 实战指南

CPython C API 类型提示对象详解:Py_GenericAlias 与 Py_GenericAliasType 实战指南

2026-09-06 12:55:31作者:江焘钦

本文基于 CPython 仓库中的官方 C API 文档 Doc/c-api/typehints.rst,系统讲解 Python 3.9 引入的 C API 类型提示对象 Py_GenericAliasPy_GenericAliasType 的语义、引用计数行为与典型用法,并结合 Objects/genericaliasobject.c 的实现源码、内建类型的 __class_getitem__ 接入方式和 Lib/test/test_genericalias.py 测试,说明如何在 C 扩展中让自定义类型支持 MyType[int] 这类 PEP 585 下标写法。

1. 背景:CPython 为类型提示提供的 C 层对象

CPython 为类型提示(type hinting)提供了一组内建类型。按 Doc/c-api/typehints.rst 的原始说明:

Various built-in types for type hinting are provided. Currently, two types exist — GenericAlias and Union. Only GenericAlias is exposed to C.

也就是说,当前与类型提示相关的内建类型有两个:

类型 对应的 Python 对象 是否暴露给 C API
GenericAlias types.GenericAliaslist[int] 这类参数化泛型) 是,通过 Py_GenericAlias / Py_GenericAliasType
Union 联合类型表达式(int | str

GenericAlias 是 PEP 585(Type Hinting Generics In Standard Collections)的产物。这一点从实现文件 Include/genericaliasobject.h 的首行注释即可确认:

// Implementation of PEP 585: support list[int] etc.
#ifndef Py_GENERICALIASOBJECT_H
#define Py_GENERICALIASOBJECT_H
...
PyAPI_FUNC(PyObject *) Py_GenericAlias(PyObject *, PyObject *);
PyAPI_DATA(PyTypeObject) Py_GenericAliasType;
...
#endif

该头文件只暴露了两个符号,这与 C API 文档的覆盖面完全一致:

  • 函数 Py_GenericAlias:在 C 层构造一个 GenericAlias 实例;
  • 类型对象 Py_GenericAliasTypePy_GenericAlias 返回对象的 C 类型,等价于 Python 层的 types.GenericAlias

Python 侧的对应关系可参考 Doc/library/types.rsttypes.GenericAlias(t_origin, t_args) 的文档:t_origin 应是未参数化的泛型类(如 listdict),t_args 是一个(长度可能为 1 的)类型元组,例如:

>>> from types import GenericAlias
>>> list[int] == GenericAlias(list, (int,))
True
>>> dict[str, int] == GenericAlias(dict, (str, int))
True

types.GenericAlias 这个名字正是由扩展模块把 C 类型对象导出而来——见 Modules/_typesmodule.c

EXPORT_STATIC_TYPE("GenericAlias", Py_GenericAliasType);

2. Py_GenericAlias:在 C 层创建 GenericAlias 对象

C API 文档给出的签名与语义如下(versionadded:: 3.9):

PyObject* Py_GenericAlias(PyObject *origin, PyObject *args);
  • 创建一个 GenericAlias 对象,等价于调用 Python 类 types.GenericAlias
  • originargs 分别成为结果的 __origin____args__ 属性;
  • origin 应当是一个 PyTypeObject*(类型对象),args 可以是 PyTupleObject*,也可以是任意 PyObject*
  • 如果传入的 args 不是元组,会自动构造一个 1 元元组,此时 __args__(args,)
  • 对参数的检查非常少(minimal checking),即使 origin 不是类型,该函数也会成功
  • __parameters__ 属性由 __args__ 惰性(lazily)构造;
  • 失败时抛出异常并返回 NULL

2.1 源码层面的行为印证

CPython 3.9 起 GenericAlias 用 C 实现。核心构造函数位于 Objects/genericaliasobject.c

PyObject *
Py_GenericAlias(PyObject *origin, PyObject *args)
{
    gaobject *alias = (gaobject*) PyType_GenericAlloc(
            (PyTypeObject *)&Py_GenericAliasType, 0);
    if (alias == NULL) {
        return NULL;
    }
    if (!setup_ga(alias, origin, args)) {
        Py_DECREF(alias);
        return NULL;
    }
    return (PyObject *)alias;
}

其中真正处理两个参数的是 setup_gaObjects/genericaliasobject.c):

static inline int
setup_ga(gaobject *alias, PyObject *origin, PyObject *args) {
    if (!PyTuple_Check(args)) {
        args = PyTuple_Pack(1, args);   // 非元组 args 自动包成 1 元元组
        if (args == NULL) {
            return 0;
        }
    }
    else {
        Py_INCREF(args);
    }

    alias->origin = Py_NewRef(origin);
    alias->args = args;
    alias->parameters = NULL;           // __parameters__ 惰性计算
    alias->weakreflist = NULL;

    if (PyVectorcall_Function(origin) != NULL) {
        alias->vectorcall = ga_vectorcall;  // origin 支持 vectorcall 时走快速调用
    }
    else {
        alias->vectorcall = NULL;
    }
    return 1;
}

从源码结构看,可以确认三点与文档描述一致且更细粒度的行为:

  1. 非元组的 args 会被 PyTuple_Pack(1, args) 包成 1 元元组,即文档所说的“自动构造 1 元元组”;
  2. 两个入参都不转移引用originPy_NewRef 增加引用,argsPy_INCREFPyTuple_Pack(新建引用)持有,调用方需要自行管理自己的引用;
  3. parameters 初始为 NULL,首次访问 __parameters__ 时才由 _Py_make_parameters__args__ 计算,对应文档中“lazy construction”的说明。

对象结构体本身为(Objects/genericaliasobject.c):

typedef struct {
    PyObject_HEAD
    PyObject *origin;
    PyObject *args;
    PyObject *parameters;
    PyObject *weakreflist;
    // Whether we're a starred type, e.g. *tuple[int].
    bool starred;
    vectorcallfunc vectorcall;
} gaobject;

其中 starred 标记用于 PEP 646 的 *tuple[int] 展开语法(对应只读成员 __unpacked__,见 Objects/genericaliasobject.cga_members)。

2.2 直接创建对象的最小示例

按 C API 文档的等价关系,在 C 层调用 Py_GenericAlias((PyObject*)&PyList_Type, args) 与 Python 层 types.GenericAlias(list, args) 等价。仓库内部本身就大量这样使用,例如 AST 代码生成器为 ast 字段类型标注生成 C 代码时(Parser/asdl_c.pyPython/Python-ast.c):

type = Py_GenericAlias((PyObject *)&PyList_Type, type);   /* 对应 list[SomeAST] */

失败路径与文档一致:PyType_GenericAlloc 分配失败或 PyTuple_Pack 失败时,Py_GenericAlias 抛出异常并返回 NULL,调用方应检查返回值。

3. 让扩展类型支持泛型:把 Py_GenericAlias 挂到 __class_getitem__

C API 文档给出的标准用法是在扩展类型的方法表里注册 __class_getitem__,直接复用 Py_GenericAlias 作为方法实现(注意 METH_O|METH_CLASS 标志组合):

...
static PyMethodDef my_obj_methods[] = {
    // Other methods.
    ...
    {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS,
     "my_obj is generic over its contained type"}
    ...
};

这样用户就可以在 Python 中写 MyObj[int] 得到 __origin__ is MyObj__args__ == (int,)GenericAlias 对象。文档同时建议参考数据模型中的 object.__class_getitem__ 方法说明。

3.1 内建类型的真实接法(同一模式)

CPython 内建类型正是按文档推荐的这一最小模式接入的,方法条目结构完全相同,只是补充了各自的 docstring:

{"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS,
 PyDoc_STR("lists are generic over the type of their contents")},

对纯 Python 类,则直接以 GenericAlias 作为类方法(见 Lib/test/test_genericalias.py 中的写法):

class MyGeneric:
    __class_getitem__ = classmethod(GenericAlias)

此外,Objects/abstract.cPyType_GetClassFromMeta 对普通类型下标的兜底实现也是调用 return Py_GenericAlias(o, key);,即类型对象 t[int] 最终都会汇聚到这个 C 函数。typing 模块中的 _SpecialForm.__getitem__ 等路径也通过 Objects/typevarobject.creturn Py_GenericAlias(op, args); 构造结果。可以说 Py_GenericAlias 是整个 PEP 585 下标机制在 C 层的统一出口。

3.2 测试层面的验证方式

Lib/test/test_genericalias.pyBaseTest.generic_types 列表(Lib/test/test_genericalias.py)收录了数十个经由 __class_getitem__ 泛型化的 C/Python 类型(re.Patternctypes.Arrayconcurrent.futures.Futurecsv.DictReader 等),并通过三个用例给出可复制的验证断言:

def test_subscriptable(self):
    for t in self.generic_types:
        alias = t[int]
        self.assertIs(alias.__origin__, t)          # __origin__ 是原类型
        self.assertEqual(alias.__args__, (int,))     # 非元组参数被包成 1 元元组
        self.assertEqual(alias.__parameters__, ())  # 无 TypeVar 时为空元组

def test_instantiate(self):
    # 泛型别名可以直接实例化:list[int]() == list()
    alias = t[int]
    self.assertEqual(alias(), t())

def test_unsubscriptable(self):
    for t in int, str, float, Sized, Hashable:
        with self.assertRaisesRegex(TypeError, tname):
            t[int]   # 未接入 __class_getitem__ 的类型会报 TypeError

这组断言与第 2 节的文档语义逐条对应,可作为 C 扩展实现 __class_getitem__ 后的验收基准。

4. Py_GenericAliasType:C 层类型对象

C API 文档同时声明了一个数据符号(versionadded:: 3.9):

PyTypeObject Py_GenericAliasType;

其含义是:Py_GenericAlias 返回对象的 C 类型,等价于 Python 中的 types.GenericAlias。它在 Objects/genericaliasobject.c 中定义:

PyTypeObject Py_GenericAliasType = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    .tp_name = "types.GenericAlias",
    .tp_doc = genericalias__doc__,
    .tp_basicsize = sizeof(gaobject),
    .tp_dealloc = ga_dealloc,
    .tp_repr = ga_repr,
    .tp_as_number = &ga_as_number,   // allow X | Y of GenericAlias objs
    .tp_as_mapping = &ga_as_mapping,
    .tp_hash = ga_hash,
    .tp_call = ga_call,
    .tp_getattro = ga_getattro,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
                Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_VECTORCALL,
    ...
    .tp_new = ga_new,
    ...
};

几个值得注意的点:

  • tp_flagsPy_TPFLAGS_BASETYPE,即 GenericAlias 可以被子类化(Python 层文档标注自 3.9.2 起可 subclass,见 Doc/library/types.rst);
  • Py_TPFLAGS_HAVE_VECTORCALL,与 gaobject.vectorcall 字段配套:当 __origin__ 支持 vectorcall 时,listint 这类实例化走 ga_vectorcall 快速路径;
  • ga_newObjects/genericaliasobject.c)要求恰好 2 个位置参数、不接受关键字参数,对应 Python 签名 GenericAlias(origin, args, /)

判断某个对象是否为 GenericAlias 时,内部代码使用 Include/internal/pycore_unionobject.h 提供的宏 _PyGenericAlias_Check(op)(展开为 PyObject_TypeCheck((op), &Py_GenericAliasType));外部扩展则可用 Py_TYPE(obj) == &Py_GenericAliasTypePy isinstance 等价手段。

5. GenericAlias 对象在 C 层支持的能力(实现佐证)

文档仅声明了构造语义,而对象自身的完整行为由 Objects/genericaliasobject.c 实现,以下均可以在源码中逐一对应:

能力 实现位置 说明
下标与部分应用 t[item] ga_getitemObjects/genericaliasobject.c 先惰性填充 __parameters__,再调用 _Py_subs_parameters 做 TypeVar 替换,最后 Py_GenericAlias(origin, newargs) 构造结果
__parameters__ 惰性属性 ga_parametersObjects/genericaliasobject.c 在临界区内由 _Py_make_parameters(alias->args) 计算并缓存,与文档“lazy”描述一致
repr ga_reprObjects/genericaliasobject.c 输出 list[int]tuple[int, ...]list[()] 等格式;starred 对象前缀 *
实例化 ga_call/ga_vectorcallObjects/genericaliasobject.c 转发给 __origin__ 构造实例,并把别名写到实例的 __orig_class__
哈希/相等 ga_hash/ga_richcompareObjects/genericaliasobject.c 相等仅比较 originargs==/!= 之外返回 NotImplemented
联合类型 X | Y tp_as_number.nb_or = _Py_union_type_orObjects/genericaliasobject.c 与文档提到的 Union 类型协同工作
继承支持 __mro_entries__Objects/genericaliasobject.c 返回 (origin,),使 class C(list[int]) 等价于继承 list
isinstance/issubclass 限制 ga_instancecheck/ga_subclasscheckObjects/genericaliasobject.c 对参数化泛型抛出 TypeError: ... argument 2 cannot be a parameterized generic
反序列化 __reduce__Objects/genericaliasobject.c 普通别名还原为 GenericAlias(origin, args)

部分应用(TypeVar 替换)规则在 _Py_subs_parameters 的注释中有规范示例(Objects/genericaliasobject.c):

/* Replace all type variables (specified by parameters)
   with corresponding values specified by argitems.
    t = list[T];          t[int]      -> newargs = [int]
    t = dict[str, T];     t[int]      -> newargs = [str, int]
    t = dict[T, list[S]]; t[str, int] -> newargs = [str, list[int]]
    t = list[[T]];        t[str]      -> newargs = [[str]]
   */

这解释了为何 Py_GenericAlias 对参数“检查很少”:真正的一致性校验(参数个数、TypeVarTuple 展开等)发生在后续下标求值阶段,例如参数不足/过多时抛出 TypeError: Too many/few arguments for ...Objects/genericaliasobject.c)。

6. Stable ABI 归属与平台导出

Py_GenericAliasPy_GenericAliasType 不仅在内建 C API 中,也属于 Stable ABI(Limited API)的一部分,两个符号均自 3.9 起可用:

这意味着在仅链接 Stable ABI 的扩展模块(Py_LIMITED_API)中也可以安全使用这两个符号,前提是目标解释器为 Python 3.9 及以上。

7. Union 类型与延伸阅读

回到文档的核心结论:类型提示相关的两个内建类型中,只有 GenericAlias 暴露给 CUnionint | str 求值结果)的实现位于 Objects/unionobject.c,其内部入口 _Py_union_type_or 正是 GenericAlias| 运算符所调用的函数,但它没有对应的公共 C API 构造器;扩展模块若需要识别联合类型,只能依赖 Python 层行为或内部 API(非公开)。

相关延伸阅读(均为仓库内路径):

8. 速查表

符号 种类 版本 作用
Py_GenericAlias(origin, args) 函数 3.9+ 创建 GenericAlias;非元组 args 自动包装为 (args,);对参数检查极少;失败返回 NULL
Py_GenericAliasType PyTypeObject 数据 3.9+ 上述对象的类型,等价于 types.GenericAlias;3.9.2 起可被 Python 子类化
{"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, doc} 方法表条目 让扩展类型获得 MyType[int] 泛型下标能力的标准写法

要点回顾:在 C 扩展中让自定义类型支持类型提示,只需在 tp_methods 中按文档示例挂接 Py_GenericAlias;若需要程序化构造泛型别名,则直接调用 Py_GenericAlias 并记得检查 NULL 返回值——这两个符号自 Python 3.9 起同时进入 Stable ABI,是 CPython 类型提示体系在 C 层的完整入口。

登录后查看全文
热门项目推荐
相关项目推荐