Following system colour scheme Selected dark colour scheme Selected light colour scheme

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 符号提供文档字符串。

动机

已经有一种定义明确的方法来为类、函数、类方法和模块提供文档:使用文档字符串。

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

然而,为了允许记录大多数这些额外的符号,已经创建了几个约定作为文档字符串内的微语法,并且目前被广泛使用:Sphinx、numpydoc、Google、Keras 等。

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

因为这些约定中的每一个都在字符串内使用微语法,所以当**编辑**这些文档字符串时,编辑器不容易提供自动完成、针对语法错误的内联错误等支持。任何针对这些约定的**编辑**支持都将建立在对编辑标准 Python 语法的支持之上。

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

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

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

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

基本原理

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

为什么这应该属于标准 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-字符串 和字符串操作。由于 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进行记录,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 在Doc 注释中使用了另一种类似的微语法变体。

但它没有特定的明确定义的微语法结构来表示哪些文档指的是哪个符号/参数,除了可以从纯 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 语言

Go 也使用一种形式的Doc 注释

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

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 被移动到另一个模块,那么将 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

上次修改时间: 2023-12-11 23:21:54 GMT