Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 727 – 经过注解的元数据中的文档

作者:
Sebastián Ramírez <tiangolo at gmail.com>
发起人:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
讨论至:
Discourse 帖子
状态:
已撤回
类型:
标准跟踪
主题:
类型标注
创建日期:
2023年8月28日
Python 版本:
3.13
发布历史:
2023年8月30日

目录

摘要

本 PEP 提议使用新的 typing.Doc 类,为使用 Annotated 定义的 Python 符号提供标准化的文档字符串。

PEP 撤回

此 PEP 的反响大多是负面的,主要 concerns 是关于冗余和可读性。因此,此 PEP 已被撤回。

动机

对于类、函数、类方法和模块,已经有了一种明确的文档记录方式:使用文档字符串。

目前,对于其他类型的符号(参数、返回值、类作用域变量(类变量和实例变量)、局部变量和类型别名),还没有正式的标准来提供文档字符串。

尽管如此,为了能够记录这些额外的符号中的大部分,已经创建了多种约定作为文档字符串内的微语法,并且目前已被普遍使用:Sphinx、numpydoc、Google、Keras 等。

在两种情况下,这些约定会得到工具的支持:对于作者,在**编辑**文档字符串内容时;对于用户,在某种程度上**渲染**该内容时(在文档网站、编辑器中的工具提示等)。

由于这些约定中的每一种都在字符串中使用微语法,在**编辑**这些文档字符串时,编辑器无法轻松地提供自动补全、针对语法错误行的内联错误等支持。对这些约定的任何**编辑**支持都将建立在对标准 Python 语法编辑支持的基础上。

在使用当前约定记录参数时,由于文档字符串在代码中的位置与实际参数不同,并且需要信息重复(参数名称),因此关于参数的信息很容易在离实际参数声明很远的代码位置,并且与其脱节。这意味着很容易重构函数、删除参数,而忘记删除其文档。添加新参数时也是如此:很容易忘记添加其文档字符串。

并且由于这种信息重复(参数名称),编辑器和其他工具需要复杂的自定义逻辑来检查或确保签名中的参数与其文档字符串中的参数保持一致,或者它们根本不支持。

由于这些现有约定是字符串内的不同类型的微语法,要**渲染**它们需要复杂的逻辑,并且需要由支持它们的工具来实现。此外,库和工具没有直接的方法在运行时获取每个单独参数或变量的文档,而无需依赖特定的文档字符串约定解析器。运行时访问参数文档字符串会很有用,例如,用于测试每个参数文档的内容,确保多个相似函数之间的一致性,或以其他方式提取并公开相同的参数文档(例如,使用 FastAPI 的 API,使用 Typer 的 CLI 等)。

其中一些以前的格式试图通过在文档字符串中包含类型信息来解决旧 Python 版本中缺乏类型注解的问题(例如 Sphinxnumpydoc),但现在这些信息不需要包含在文档字符串中,因为现在有了官方的类型注解语法

基本原理

本提案旨在解决这些不足之处,通过扩展和补充文档字符串中的信息,保持与现有文档字符串的向后兼容性(不弃用它们),并以一种利用 Python 语言和结构的方式进行,通过使用 Annotated 的类型注解,以及 typing 中新的 Doc 类。

此功能之所以属于标准 Python 库而不是外部包,是因为尽管实现起来非常简单,但其真正的强大功能和优势将来自于其标准化,从而方便库作者使用,并提供一种使用 Annotated 来记录 Python 符号的默认方式。一些工具提供商(至少是 VS Code 和 PyCharm)已表示,只有在成为标准时,他们才会考虑实现支持。

这并不会弃用当前对文档字符串的使用,文档字符串应被视为首选的文档记录方法(在类型别名、参数等中不可用时)。并且文档字符串将通过此提案得到补充,用于记录可以与 Annotated 声明的符号相关的特定文档(目前仅由多种可用的微语法约定涵盖)。

对于普通开发者(库用户)来说,这应该是相对透明的,除非他们手动打开采用它的库的源代码。

对于希望采用它的库作者来说,这应该是选择加入的,他们应该自由决定是否使用。

它仅对愿意使用可选类型提示的库有用。

总结

以下是此提案的特性与当前约定相比的简要概述

  • **编辑**将已获得支持,任何支持 Python 语法的编辑器(当前或未来)默认完全支持,包括语法错误、语法高亮等。
  • **渲染**将相对直接地实现由静态工具(不需要运行时执行)来支持,因为信息可以从它们通常已创建的 AST 中提取。
  • 信息去重:参数的名称将在一个地方定义,而不是在文档字符串中重复。
  • 消除了删除参数或类变量而忘记删除其文档而导致不一致的可能性。
  • 最大程度地减少了添加新参数或类变量而忘记添加其文档的可能性。
  • 当参数被重命名时,消除了签名中的参数名称与其文档字符串中的名称不一致的可能性。
  • 在运行时访问每个符号的文档字符串,包括现有的(较旧的)Python 版本。
  • 一种更正式的方式来记录其他符号,如类型别名,可以使用 Annotated
  • 新入门者无需学习微语法,这只是 Python 语法。
  • ParamSpec 捕获的函数的参数文档继承。

规范

主要提议是引入一个新的类,typing.Doc。此类别只能在 Annotated 注解中使用。它接受一个位置参数字符串。它应该用于记录使用 Annotated 声明的符号的预期含义和用途。

例如

from typing import Annotated, Doc

class User:
    name: Annotated[str, Doc("The user's name")]
    age: Annotated[int, Doc("The user's age")]

    ...

Annotated 通常用作类型注解,在这种情况下,其中任何 typing.Doc 都将记录被注解的符号。

Annotated 用于声明类型别名时,typing.Doc 将用于记录类型别名符号。

例如

from typing import Annotated, Doc, TypeAlias

from external_library import UserResolver

CurrentUser: TypeAlias = Annotated[str, Doc("The current system user"), UserResolver()]

def create_user(name: Annotated[str, Doc("The user's name")]): ...

def delete_user(name: Annotated[str, Doc("The user to delete")]): ...

在这种情况下,如果用户导入了 CurrentUser,像编辑器这样的工具可以在用户将鼠标悬停在该符号上时提供带有文档字符串的工具提示,或者文档工具可以在其生成的输出中包含带有文档的类型别名。

对于在运行时提取信息的工具,它们通常会使用 get_type_hints() 并带有参数 include_extras=True,并且由于 Annotated 是规范化的(即使带有类型别名),这意味着它们应该使用最后一个可用的 typing.Doc,如果有多个 typing.Doc 被使用,因为这是最后一个使用的。

在运行时,typing.Doc 实例具有一个 documentation 属性,其中包含传递给它的字符串。

当函数的签名被 ParamSpec 捕获时,应保留与参数关联的任何文档字符串。

任何处理 typing.Doc 对象的工具都应将字符串解释为文档字符串,因此应像使用 inspect.cleandoc() 一样规范化空格。

传递给 typing.Doc 的字符串应为有效的文档字符串形式。这意味着不应使用f-string和字符串操作。由于 Python 运行时无法强制执行此操作,因此工具不应依赖此行为。

当提供**渲染**的工具显示原始签名时,它们可以允许配置是否显示整个原始 Annotated 代码,但默认情况下它们不应包含 Annotated 及其内部代码元数据,而应仅显示被注解符号的类型。当这些工具支持 typing.Doc 并以其他方式渲染(而不仅仅是原始签名)时,它们应以方便的方式显示传递给 typing.Doc 的字符串值,显示被文档化的符号和文档字符串之间的关系。

提供**渲染**的工具可以提供配置在哪里以不同方式显示参数文档和文本文档的方法。否则,它们可以简单地先显示文本文档,然后显示参数文档。

示例

类属性可以被记录

from typing import Annotated, Doc

class User:
    name: Annotated[str, Doc("The user's name")]
    age: Annotated[int, Doc("The user's age")]

    ...

函数或方法的参数和返回值也可以被记录

from typing import Annotated, Doc

def create_user(
    name: Annotated[str, Doc("The user's name")],
    age: Annotated[int, Doc("The user's age")],
    cursor: DatabaseConnection | None = None,
) -> Annotated[User, Doc("The created user after saving in the database")]:
    """Create a new user in the system.

    It needs the database connection to be already initialized.
    """
    pass

向后兼容性

此提案与现有代码完全向后兼容,并且不弃用现有文档字符串约定的使用。

对于希望在标准库可用之前采用此提案,或支持旧版 Python 的开发者,他们可以使用 typing_extensions 并从那里导入和使用 Doc

例如

from typing import Annotated
from typing_extensions import Doc

class User:
    name: Annotated[str, Doc("The user's name")]
    age: Annotated[int, Doc("The user's age")]

    ...

安全隐患

没有已知的安全影响。

如何教授此内容

主要的文档机制应继续是用于文本信息的标准文档字符串,这适用于模块、类、函数和方法。

对于希望采用此提案以增加粒度的作者,他们可以在支持它的符号的 Annotated 注解中使用 typing.Doc

希望在保持与旧版 Python 向后兼容的同时采用此提案的库作者,应使用 typing_extensions.Doc 而不是 typing.Doc

参考实现

typing.Doc 的实现等同于

class Doc:
    def __init__(self, documentation: str, /):
        self.documentation = documentation

它已在 typing_extensions 包中实现。

其他语言的调查

这里简要调查了其他语言如何记录其符号。

Java

Java 函数及其参数使用 Javadoc 进行文档记录,这是一种特殊的注释格式,放在函数定义之上。这类似于 Python 当前的文档字符串微语法约定(但只有一个)。

例如

/**
* Returns an Image object that can then be painted on the screen.
* The url argument must specify an absolute <a href="#{@link}">{@link URL}</a>. The name
* argument is a specifier that is relative to the url argument.
* <p>
* This method always returns immediately, whether or not the
* image exists. When this applet attempts to draw the image on
* the screen, the data will be loaded. The graphics primitives
* that draw the image will incrementally paint on the screen.
*
* @param  url  an absolute URL giving the base location of the image
* @param  name the location of the image, relative to the url argument
* @return      the image at the specified URL
* @see         Image
*/
public Image getImage(URL url, String name) {
  try {
    return getImage(new URL(url, name));
  } catch (MalformedURLException e) {
    return null;
  }
}

JavaScript

JavaScript 和 TypeScript 都使用与 Javadoc 类似的系统。

JavaScript 使用 JSDoc

例如

/**
* Represents a book.
* @constructor
* @param {string} title - The title of the book.
* @param {string} author - The author of the book.
*/
function Book(title, author) {
}

TypeScript

TypeScript 有自己的 JSDoc 参考,其中包含一些变体。

例如

// Parameters may be declared in a variety of syntactic forms
/**
* @param {string}  p1 - A string param.
* @param {string=} p2 - An optional param (Google Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @returns {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4) {
    // TODO
}

Rust

Rust 在文档注释中使用另一种类似的微语法变体。

但它没有特别明确定义的微语法结构来指示什么文档指向什么符号/参数,除了可以通过纯 Markdown 推断出的内容。

例如

#![crate_name = "doc"]

/// A human being is represented here
pub struct Person {
   /// A person must have a name, no matter how much Juliet may hate it
   name: String,
}

impl Person {
   /// Returns a person with the name given them
   ///
   /// # Arguments
   ///
   /// * `name` - A string slice that holds the name of the person
   ///
   /// # Examples
   ///
   /// ```
   /// // You can have rust code between fences inside the comments
   /// // If you pass --test to `rustdoc`, it will even test it for you!
   /// use doc::Person;
   /// let person = Person::new("name");
   /// ```
   pub fn new(name: &str) -> Person {
      Person {
            name: name.to_string(),
      }
   }

   /// Gives a friendly hello!
   ///
   /// Says "Hello, [name](Person::name)" to the `Person` it is called on.
   pub fn hello(& self) {
      println!("Hello, {}!", self.name);
   }
}

fn main() {
   let john = Person::new("John");

   john.hello();
}

Go Lang

Go 也使用一种形式的文档注释

它没有明确定义的微语法结构来指示什么文档指向什么符号/参数,但参数可以通过名称引用而无需任何特殊语法或标记,这也意味着普通单词如果可能出现在文档文本中,应避免作为参数名称。

package strconv

// Quote returns a double-quoted Go string literal representing s.
// The returned string uses Go escape sequences (\t, \n, \xFF, \u0100)
// for control characters and non-printable characters as defined by IsPrint.
func Quote(s string) string {
   ...
}

被拒绝的想法

标准化当前文档字符串

一个可能的替代方案是支持并尝试将现有的文档字符串格式之一推向标准。但这只会解决标准化问题。

它不会解决因在文档字符串中使用微语法而不是纯 Python 语法而产生的任何其他问题,正如在**理由 - 摘要**部分所述。

额外元数据和装饰器

在此提案之前的一些想法包括使用一个函数 doc() 而不是单个类 Doc,该函数带有几个参数来指示对象是否不鼓励使用、它可能引发的异常等。为了也能弃用函数和类,还期望 doc() 可以用作装饰器。但此功能由 PEP 702 中的 typing.deprecated() 覆盖,因此已从该提案中删除。

在未来,声明附加信息的方式仍然可能有用,但根据对此想法的早期反馈,所有这些都已推迟到未来的提案。

这也将重点从一个包含多个参数的包罗万象的函数 doc() 转移到一个单独的 Doc 类,该类用于 Annotated,以便与未来的提案进行组合。

此设计更改还允许与其他提案(如 typing.deprecated())更好地互操作,因为未来可以考虑允许在 Annotated 中也有 typing.deprecated() 来弃用单个参数,与 Doc 共存。

定义下的字符串

讨论中提出的一个可能替代方案是声明一个字符串在符号定义下方,并提供对这些值的运行时访问。

class User:
    name: str
    "The user's name"
    age: int
    "The user's age"

    ...

这在 PEP 224 中已被提出并拒绝,主要原因是字符串如何与其文档化的符号相关联的模糊性。

此外,在旧版 Python 中无法提供对此值的运行时访问。

注解中的纯字符串

在讨论中,还建议使用 Annotated 中的纯字符串。

from typing import Annotated

class User:
    name: Annotated[str, "The user's name"]
    age: Annotated[int, "The user's age"]

    ...

但这将为 Annotated 中的任何纯字符串赋予预定义含义,并且任何使用纯字符串用于其他目的(目前是允许的)的工具现在都将无效。

拥有显式的 typing.Doc 使其与当前有效的 Annotated 用途兼容。

另一种类似注解的类型

在讨论中,建议定义一个类似于 Annotated 的新类型,它接受类型和一个带有文档字符串的参数。

from typing import Doc

class User:
    name: Doc[str, "The user's name"]
    age: Doc[int, "The user's age"]

    ...

这个想法被拒绝了,因为它只会支持该用例,并且会使其更难与 Annotated 结合用于其他目的(例如,与 FastAPI 元数据、Pydantic 字段等)或添加文档字符串以外的其他元数据(例如,弃用)。

从类型别名转移文档

此提案的早期版本规定,当使用用 Annotated 声明的类型别名,并且这些类型别名在注解中使用时,文档字符串将被转移到被注解的符号。

例如

from typing import Annotated, Doc, TypeAlias


UserName: TypeAlias = Annotated[str, Doc("The user's name")]


def create_user(name: UserName): ...

def delete_user(name: UserName): ...

在收到提供编辑器支持的主要组件之一的维护者的反馈后,这一点被拒绝了。

使用切片的简写

在讨论中,有人建议使用切片的简写。

is_approved: Annotated[str: "The status of a PEP."]

虽然这是一个非常巧妙的主意,并且可以消除对新 Doc 类的需求,但当前版本的 Python 在运行时不允许这样做。

在运行时,Annotated 需要至少两个参数,并且要求第一个参数是类型,如果它是切片,它就会崩溃。

未解决的问题

冗余

反对这一点的主要论点是增加了冗余。

如果签名不独立于文档查看,并且函数体与文档字符串一起被衡量,那么总的冗余度将有些相似,因为此提案所做的就是将文档字符串内容的一部分从函数体移到签名中。

仅考虑签名,而不考虑函数体,它们可能比当前的长得多,甚至可能超过一页。作为交换,当前超过一页的等效文档字符串将大大缩短。

在比较总冗余度时,包括签名和文档字符串,此提案增加的主要冗余度将来自使用 Annotatedtyping.Doc。如果 Annotated 的使用更广泛,那么为它和它将携带的元数据类型提供改进的简短语法是有意义的。但这只有在 Annotated 被更广泛使用后才有意义。

另一方面,这种冗余不会影响最终用户,因为他们不会看到使用 typing.Doc 的内部代码。大多数用户将通过编辑器与库进行交互,而无需查看内部信息,即使有,他们也会获得支持此提案的编辑器的工具提示。

处理额外冗余的成本将主要由使用此功能的库维护者承担。

这个论点可以类比于反对类型注解的论点,因为它们确实增加了冗余,以换取它们的功能。但是,与类型注解一样,这将是可选的,并且只供那些愿意承担额外冗余以换取好处的人使用。

当然,更高级的用户可能想查看库的源代码,如果那些库的作者采用了这个提案,那么这些高级用户最终将不得不查看包含附加签名冗余的代码,而不是文档字符串冗余。

任何决定不采用它的作者都应该可以自由地继续使用具有他们决定的任何特定格式的文档字符串,或者根本不使用文档字符串,等等。

不过,如果该提案成为官方推荐的解决方案,库作者很有可能会受到采用它的压力。

文档不是类型注解

也可以认为文档本身并不是类型注解的一部分,或者它应该驻留在不同的模块中。或者这些信息不应该成为签名的一部分,而应该驻留在其他地方(如文档字符串)。

然而,Python 中的类型注解默认可以被视为附加元数据:它们携带关于变量、参数、返回类型等的附加信息,并且默认情况下它们没有任何运行时行为。此提案将一种新的元数据类型添加到其中。

可以认为此提案扩展了类型注解所携带的信息类型,就像 PEP 702 将它们扩展以包含弃用信息一样。

Annotated 被添加到标准库中,正是为了支持向注解添加附加元数据,并且由于新的提议的 Doc 类与 Annotated 紧密耦合,因此它应该与 Annotated living 在同一个模块中。如果 Annotated 被移到另一个模块,那么 Doc 也应该一起移动。

多重标准

另一个反对的论点是它会创造另一个标准,并且已经存在几种文档字符串约定。似乎最好是正式化当前已有的标准之一。

然而,如上所述,这些约定都没有解决此提案自然解决的基于文档字符串方法的普遍弊端。

要查看基于文档字符串方法的弊端列表,请参阅上面的**理由 - 摘要**部分。

同样,可以看出,在许多情况下,一个利用新特性并解决先前方法中的许多问题的标准是值得拥有的。就像新的 pyproject.tomldataclass_transform,新的类型管道/联合(|)运算符,以及其他情况。

采用

由于这是一个新的标准提案,只有在获得社区的兴趣时才算有意义。

幸运的是,包括 FastAPI、Typer、SQLModel、Asyncer(此提案的作者)、Pydantic、Strawberry(GraphQL)等在内的几个主流库已经表示出兴趣。

文档工具也表示出兴趣和支持,例如 mkdocstrings,它甚至增加了对该提案早期版本的支持。

联系征求早期反馈的所有 CPython 核心开发者(至少 4 名)都对此提案表示出兴趣和支持。

编辑器开发者(VS Code 和 PyCharm)表示出一些兴趣,同时也对该提案的签名冗余表示担忧,但并未对实现(这将对他们影响最大)表示担忧。他们表示,如果该提案成为官方标准,他们会考虑添加支持。在这种情况下,他们只需要添加对渲染的支持,因为对编辑的支持(通常对其他标准来说是不存在的)已经存在,因为他们已经支持编辑标准 Python 语法。


来源:https://github.com/python/peps/blob/main/peps/pep-0727.rst

最后修改:2025-05-06 20:44:17 GMT