【问题标题】:Why does Python's grammar specification not include docstrings and comments?为什么 Python 的语法规范不包括文档字符串和注释?
【发布时间】:2017-11-26 19:23:59
【问题描述】:

我正在咨询官方Python grammar specification as of Python 3.6

我无法找到 cmets(它们以# 开头)和文档字符串(它们应以''' 开头)的任何语法。快速查看the lexical analysis 页面也没有帮助 - 文档字符串在那里定义为longstrings,但不会出现在语法规范中。一个名为 STRING 的类型出现得更远,但没有对其定义的引用发生。

鉴于此,我很好奇 CPython 编译器如何知道 cmets 和 docstrings 是什么。这一壮举是如何完成的?

我最初猜想 cmets 和 docstrings 会在 CPython 编译器的第一次传递中被删除,但随后就提出了 help() 如何能够呈现相关 docstrings 的问题。

【问题讨论】:

    标签: python grammar python-internals


    【解决方案1】:

    文档字符串不是一个单独的语法实体。它只是一个普通的simple_stmt(遵循该规则一直到atomSTRING+ *。如果它是函数体中的 first 语句,类或模块,然后它使用作为编译器的文档字符串。

    这在参考文档中记录为classdef 复合语句的脚注:

    [3] 在函数体中作为第一条语句出现的字符串文字被转换为函数的__doc__ 属性,从而转换为函数的文档字符串。

    [4] 出现在类主体中的第一条语句的字符串文字被转换为命名空间的__doc__ 项,从而转换为类的文档字符串。

    目前没有为模块指定相同的参考文档,我认为这是一个文档错误。

    注释被分词器删除,永远不需要被解析为语法。他们的整个是在语法层面上没有意义。请参阅词法分析文档的Comments section

    注释以不属于字符串文字的井号字符 (#) 开头,并在物理行的末尾结束。除非调用隐式行连接规则,否则注释表示逻辑行的结束。 注释被语法忽略;它们不是令牌

    我的大胆强调。所以tokenizer 完全跳过了 cmets:

    /* Skip comment */
    if (c == '#') {
        while (c != EOF && c != '\n') {
            c = tok_nextc(tok);
        }
    }
    

    请注意,Python 源代码经过 3 个步骤:

    1. 标记化
    2. 解析
    3. 编译

    语法只适用于解析阶段; cmets 在分词器中被删除,文档字符串仅对编译器是特殊的。

    为了说明解析器如何不将文档字符串视为字符串文字表达式以外的任何内容,您可以通过ast module抽象语法树的形式访问任何 Python 解析结果。这会生成 Python 对象,这些对象直接反映 Python 语法解析器生成的解析树,然后从中编译 Python 字节码:

    >>> import ast
    >>> function = 'def foo():\n    "docstring"\n'
    >>> parse_tree = ast.parse(function)
    >>> ast.dump(parse_tree)
    "Module(body=[FunctionDef(name='foo', args=arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Str(s='docstring'))], decorator_list=[], returns=None)])"
    >>> parse_tree.body[0]
    <_ast.FunctionDef object at 0x107b96ba8>
    >>> parse_tree.body[0].body[0]
    <_ast.Expr object at 0x107b16a20>
    >>> parse_tree.body[0].body[0].value
    <_ast.Str object at 0x107bb3ef0>
    >>> parse_tree.body[0].body[0].value.s
    'docstring'
    

    所以你有FunctionDef 对象,作为主体中的第一个元素,它有一个表达式,它是一个Str,其值为'docstring'编译器然后生成一个代码对象,将该文档字符串存储在一个单独的属性中。

    您可以使用compile() function 将AST 编译成字节码;同样,这是使用 Python 解释器使用的实际代码路径。我们将使用dis module 为我们反编译字节码:

    >>> codeobj = compile(parse_tree, '', 'exec')
    >>> import dis
    >>> dis.dis(codeobj)
      1           0 LOAD_CONST               0 (<code object foo at 0x107ac9d20, file "", line 1>)
                  2 LOAD_CONST               1 ('foo')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (foo)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    

    因此,编译后的代码生成了模块的顶级语句。 MAKE_FUNCTION opcode 使用存储的代码对象(顶级代码对象常量的一部分)来构建函数。因此,我们查看索引 0 处的嵌套代码对象:

    >>> dis.dis(codeobj.co_consts[0])
      1           0 LOAD_CONST               1 (None)
                  2 RETURN_VALUE
    

    这里的文档字符串似乎消失了。该函数只返回None。文档字符串被存储为常量:

    >>> codeobj.co_consts[0].co_consts
    ('docstring', None)
    

    当执行MAKE_FUNCTION操作码时,如果它是一个字符串,它就是第一个常量,它被转换成函数对象的__doc__属性。

    编译后,我们可以将带有exec() function 的代码对象执行到给定的命名空间中,这会添加一个带有文档字符串的函数对象:

    >>> namespace = {}
    >>> exec(codeobj, namespace)
    >>> namespace['foo']
    <function foo at 0x107c23e18>
    >>> namespace['foo'].__doc__
    'docstring'
    

    所以编译器的工作是确定什么时候是文档字符串。这是在 C 代码中完成的,在 compiler_isdocstring() function:

    static int
    compiler_isdocstring(stmt_ty s)
    {
        if (s->kind != Expr_kind)
            return 0;
        if (s->v.Expr.value->kind == Str_kind)
            return 1;
        if (s->v.Expr.value->kind == Constant_kind)
            return PyUnicode_CheckExact(s->v.Expr.value->v.Constant.value);
        return 0;
    }
    

    这是从文档字符串有意义的位置调用的;对于模块和类,在 compiler_body() 中,对于函数,在 compiler_function() 中。


    TLDR:cmets 不是语法的一部分,因为语法解析器甚至从未看到 cmets。它们被标记器跳过。文档字符串不是语法的一部分,因为对于语法解析器来说,它们只是字符串文字。正是编译步骤(获取解析器的解析树输出)将这些字符串表达式解释为文档字符串。


    *完整的语法规则路径是simple_stmt -> small_stmt -> expr_stmt -> testlist_star_expr -> star_expr -> expr -> xor_expr -> and_expr -> shift_expr -> arith_expr -> term -> factor -> power -> atom_expr -> atom -> STRING+

    【讨论】:

    • 某处是否有“真正的”完整语法规范?如果我想查看 Python cmets 的外观怎么办? “完整语法规范”页面是否还缺少其他内容?
    • 我尝试通过谷歌搜索python tokenizer 获取完整图片,但只找到了tokenize 模块。一定不是这样,因为它的文档说 “此模块中的扫描仪也将 cmets 作为令牌返回,这对于实现“漂亮的打印机”很有用,包括用于屏幕显示的着色器。”
    • @StefanPochmann:tokenize 模块与 C implementation 相呼应,增加了一些细节,例如将 cmets 视为令牌无论如何
    • @StefanPochmann:完整的语法是full grammar 用作pgen parser generator 的输入。如果您想了解这一切是如何运作的,请参阅eli.thegreenplace.net/2010/06/30/…
    【解决方案2】:

    第 1 节

    cmets 会怎样?

    注释(任何以# 开头的内容)在标记化/词法分析期间会被忽略,因此无需编写规则来解析它们。它们不向解释器/编译器提供任何语义信息,因为它们只是为了读者而提高程序的冗长性,因此它们被忽略了。

    这是 ANSI C 编程语言的 lex 规范:http://www.quut.com/c/ANSI-C-grammar-l-1998.html。我想提请您注意这里处理 cmets 的方式:

    "/*"            { comment(); }
    "//"[^\n]*      { /* consume //-comment */ }
    

    现在,看看int 的规则。

    "int"           { count(); return(INT); }
    

    这是处理 int 和其他标记的 lex 函数:

    void count(void)
    {
        int i;
    
        for (i = 0; yytext[i] != '\0'; i++)
            if (yytext[i] == '\n')
                column = 0;
            else if (yytext[i] == '\t')
                column += 8 - (column % 8);
            else
                column++;
    
        ECHO;
    }
    

    你在这里看到它以ECHO 语句结尾,这意味着它是一个有效的令牌,必须被解析。

    现在,这是处理 cmets 的 lex 函数:

    void comment(void)
    {
        char c, prev = 0;
    
        while ((c = input()) != 0)      /* (EOF maps to 0) */
        {
            if (c == '/' && prev == '*')
                return;
            prev = c;
        }
        error("unterminated comment");
    }
    

    这里没有ECHO。所以,什么都没有返回。

    这是一个有代表性的例子,但是 python 做的完全一样。


    第 2 节

    文档字符串会发生什么?

    注意:我回答的这一部分是对@MartijnPieters 回答的补充。这并不意味着复制他在帖子中提供的任何信息。现在,话虽如此,...

    我最初猜想 cmets 和 docstrings 是在一个 首先通过 CPython 编译器[...]

    Docstrings(未分配给任何变量名的字符串文字,'...'"..."'''...'''"""...""" 中的任何内容)确实被处理了。正如 Martijn Pieters 在 his answer 中提到的那样,它们被解析为简单的字符串文字(STRING+ 令牌)。在当前文档中,只是顺便提到将文档字符串分配给函数/类/模块的__doc__ 属性。在任何地方都没有真正深入地提到它是如何完成的。

    实际发生的是,它们被标记化并解析为字符串文字,生成的解析树将包含它们。从解析树生成字节码,文档字符串在__doc__ 属性中的正确位置(它们不是字节码的明确部分,如下图所示)。我不会详细说明,因为我上面链接的答案非常详细地描述了相同的内容。

    当然,完全可以忽略它们。如果您使用python -OO-OO 标志代表“强烈优化”,而-O 代表“适度优化”),生成的字节码存储在.pyo 文件中,其中不包括文档字符串.

    如下图所示:

    使用以下代码创建文件test.py

    def foo():
        """ docstring """
        pass
    

    现在,我们将编译这段代码并设置正常的标志。

    >>> code = compile(open('test.py').read(), '', 'single')
    >>> import dis
    >>> dis.dis(code)
      1           0 LOAD_CONST               0 (<code object foo at 0x102b20ed0, file "", line 1>)
                  2 LOAD_CONST               1 ('foo')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (foo)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    

    如您所见,字节码中没有提及我们的文档字符串。然而,他们那里。要获取文档字符串,您可以...

    >>> code.co_consts[0].co_consts
    (' docstring ', None)
    

    因此,如您所见,文档字符串 确实 保留,只是不作为主字节码的一部分。现在,让我们重新编译这段代码,但优化级别设置为 2(相当于-OO 开关):

    >>> code = compile(open('test.py').read(), '', 'single', optimize=2)
    >>> dis.dis(code)
      1           0 LOAD_CONST               0 (<code object foo at 0x102a95810, file "", line 1>)
                  2 LOAD_CONST               1 ('foo')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (foo)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    

    不,区别,但是...

    >>> code.co_consts[0].co_consts
    (None,)
    

    文档字符串现在已经消失了。

    -O-OO 标志只删除东西(默认情况下会优化字节码...-O 从生成的字节码中删除断言语句和 if __debug__: 套件,而 -OO 忽略文档字符串添加)。结果编译时间将略有减少。另外,执行速度保持不变,除非你有大量的assertif __debug__:语句,否则对性能没有影响。

    另外,请记住,仅当文档字符串是函数/类/模块定义中的第一件事时,才会保留这些文档字符串。在编译过程中,所有额外的字符串都会被简单地删除。如果您将test.py 更改为以下内容:

    def foo():
        """ docstring """
    
        """test"""
        pass
    

    然后用optimization=0重复同样的过程,编译时保存在co_consts变量中:

    >>> code.co_consts[0].co_consts
    (' docstring ', None)
    

    意思是,""" test """ 已被忽略。您会感兴趣的是,此删除是作为字节码基础优化的一部分完成的。


    第 3 节

    补充阅读

    (您可能会发现这些参考资料和我一样有趣。)

    1. What does Python optimization (-O or PYTHONOPTIMIZE) do?

    2. What do the python file extensions, .pyc .pyd .pyo stand for?

    3. Are Python docstrings and comments stored in memory when a module is loaded?

    4. Working with compile()

    5. dis 模块

    6. peephole.c(由 Martijn 提供)- 所有编译器优化的源代码。如果你能理解的话,这尤其令人着迷!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-18
      • 2020-09-23
      • 1970-01-01
      • 2015-02-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多