为什么Python的语法规范不包含文档字符串和注释?

Aks*_*jan 3 python grammar python-internals

我正在咨询Python 3.6的官方Python语法规范.

我无法找到任何注释语法(它们显示在a前面#)和文档字符串(它们应该出现''').快速查看词法分析页面也没有帮助 - 文档字符串在那里被定义,longstrings但没有出现在语法规范中.名称的类型会STRING进一步显示,但不会引用其定义.

鉴于此,我很好奇CPython编译器如何知道注释和文档字符串是什么.这项壮举是如何完成的?

我最初猜测,CPython编译器在第一次传递中删除了注释和文档字符串,但随后又乞求了如何help()呈现相关文档字符串的问题.

Mar*_*ers 7

docstring不是一个单独的语法实体.它只是一个常规的simple_stmt(遵循该规则一直到*atom和.如果它是函数体,类或模块中的第一个语句,那么它编译器用作文档字符串.STRING+

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

[3]作为函数体中第一个语句出现的字符串文字被转换为函数的__doc__属性,因此转换为函数的docstring.

[4]作为类体中第一个语句出现的字符串文字被转换为命名空间的__doc__项,因此转换为类的docstring.

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

标记器删除注释,永远不需要将其解析为语法.他们的全部意义是在语法层面没有意义.请参阅Lexical Analysis文档的" 注释"部分:

注释以散列字符(#)开头,该散列字符不是字符串文字的一部分,并在物理行的末尾结束.注释表示逻辑行的结束,除非调用隐式行连接规则.语法忽略注释; 他们不是代币.

大胆强调我的.因此,标记生成器完全跳过评论:

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

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

  1. 符号化
  2. 解析
  3. 汇编

语法仅适用于解析阶段; 注释将在tokenizer中删除,docstrings仅对编译器有用.

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

>>> 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'
Run Code Online (Sandbox Code Playgroud)

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

您可以使用该compile()函数将AST编译为字节码; 再次,这是使用Python解释器使用的实际代码路径.我们将使用该dis模块为我们反编译字节码:

>>> 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
Run Code Online (Sandbox Code Playgroud)

因此,编译后的代码生成了模块的顶级语句.所述MAKE_FUNCTION操作码使用存储codeobject(顶层代码对象常数的一部分)来构建的功能.所以我们在索引0处查看嵌套代码对象:

>>> dis.dis(codeobj.co_consts[0])
  1           0 LOAD_CONST               1 (None)
              2 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

这里的docstring似乎已经消失了.该功能只是返回None.而docstring则存储为常量:

>>> codeobj.co_consts[0].co_consts
('docstring', None)
Run Code Online (Sandbox Code Playgroud)

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

编译完成后,我们可以将代码对象与exec()函数一起执行到给定的命名空间中,这会添加一个带有docstring的函数对象:

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

所以编译器的工作就是确定什么是文档字符串.这是在C代码中完成的,在compiler_isdocstring()函数中:

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;
}
Run Code Online (Sandbox Code Playgroud)

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


TLDR:注释不是语法的一部分,因为语法分析器甚至看不到注释.标记生成器会跳过它们.Docstrings不是语法的一部分,因为对于语法分析器,它们只是字符串文字.编译步骤(采用解析器的解析树输出)将这些字符串表达式解释为docstrings.


*完整的语法规则路径是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+


cs9*_*s95 6

第1节

评论会怎样?

#在标记化/词法分析期间忽略注释(以a 开头的任何内容),因此不需要编写规则来解析它们.它们不向解释器/编译器提供任何语义信息,因为它们仅用于为读者提高程序的详细程度,因此它们被忽略.

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

"/*"            { comment(); }
"//"[^\n]*      { /* consume //-comment */ }
Run Code Online (Sandbox Code Playgroud)

现在,看一下规则吧int.

"int"           { count(); return(INT); }
Run Code Online (Sandbox Code Playgroud)

这是处理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;
}
Run Code Online (Sandbox Code Playgroud)

你在这里看到它以ECHO语句结束,这意味着它是一个有效的标记,必须进行解析.

现在,这是处理注释的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");
}
Run Code Online (Sandbox Code Playgroud)

这里没有ECHO.所以,没有任何回报.

这是一个有代表性的例子,但python完全相同.


第2节

docstrings会发生什么?

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

我最初猜测CPython编译器在第一遍中删除了注释和文档字符串[...]

文档字符串(未分配给任何变量名字符串常量,内什么'...',"...",'''...''',或"""...""")确实处理.STRING+正如Martijn Pieters在他的回答中提到的那样,它们被解析为简单的字符串文字(标记).从当前的文档开始,只是顺便提一下,文档字符串被赋值给函数/ class/module的__doc__属性.如何做到并没有在任何地方深入提及.

实际发生的是它们被标记化并解析为字符串文字,生成的结果解析树将包含它们.从解析树生成字节代码,文档字符串位于__doc__属性中的合法位置(它们不是字节代码的明确部分,如下所示).我不会详细介绍,因为上面链接的答案描述的内容非常详细.

当然,可以完全忽略它们.如果你使用python -OO(-OO标志代表"强烈优化",而不是-O代表"温和地优化"),结果字节代码存储在.pyo文件中,排除了文档字符串.

下图可以看到:

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

def foo():
    """ docstring """
    pass
Run Code Online (Sandbox Code Playgroud)

现在,我们将使用正常的标志集编译此代码.

>>> 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
Run Code Online (Sandbox Code Playgroud)

如您所见,字节代码中没有提到我们的docstring.但是,他们那里.要获得文档字符串,您可以...

>>> code.co_consts[0].co_consts
(' docstring ', None)
Run Code Online (Sandbox Code Playgroud)

因此,正如您所看到的,docstring 确实保留,而不是作为主字节码的一部分.现在,让我们重新编译这段代码,但优化级别设置为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
Run Code Online (Sandbox Code Playgroud)

不,差异,但......

>>> code.co_consts[0].co_consts
(None,)
Run Code Online (Sandbox Code Playgroud)

docstrings现在已经消失了.

-O-OO标志只删除的东西(的字节代码优化默认情况下,完成了... -O移除了断言语句,并if __debug__:从生成的字节码套房,而-OO另外忽略文档字符串).结果编译时间会略有减少.此外,执行的速度保持不变,除非你有大量的assertif __debug__:声明,否则使性能没有区别.

另外,请记住只有在文档字符串是函数/类/模块定义中的第一个内容时才会保留文档字符串.编译期间只删除所有其他字符串.如果您更改test.py为以下内容:

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

    """test"""
    pass
Run Code Online (Sandbox Code Playgroud)

然后重复相同的过程optimization=0,这是co_consts在编译时存储在变量中:

>>> code.co_consts[0].co_consts
(' docstring ', None)
Run Code Online (Sandbox Code Playgroud)

意思,""" test """一直被忽视.您会感兴趣的是,此删除操作是字节代码基本优化的一部分.


第3节

补充阅读

(你可能会发现这些引用和我一样有趣.)

  1. Python优化(-O或PYTHONOPTIMIZE)有什么作用?

  2. python文件扩展名是什么,.pyc .pyd .pyo代表什么?

  3. 加载模块时,Python文档字符串和注释是否存储在内存中?

  4. 使用compile()

  5. dis模块

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