文档字符串不是一个单独的语法实体。它只是一个普通的simple_stmt(遵循该规则一直到atom 和STRING+ *。如果它是函数体中的 first 语句,类或模块,然后它使用作为编译器的文档字符串。
这在参考文档中记录为class 和def 复合语句的脚注:
[3] 在函数体中作为第一条语句出现的字符串文字被转换为函数的__doc__ 属性,从而转换为函数的文档字符串。
[4] 出现在类主体中的第一条语句的字符串文字被转换为命名空间的__doc__ 项,从而转换为类的文档字符串。
目前没有为模块指定相同的参考文档,我认为这是一个文档错误。
注释被分词器删除,永远不需要被解析为语法。他们的整个点是在语法层面上没有意义。请参阅词法分析文档的Comments section:
注释以不属于字符串文字的井号字符 (#) 开头,并在物理行的末尾结束。除非调用隐式行连接规则,否则注释表示逻辑行的结束。 注释被语法忽略;它们不是令牌。
我的大胆强调。所以tokenizer 完全跳过了 cmets:
/* Skip comment */
if (c == '#') {
while (c != EOF && c != '\n') {
c = tok_nextc(tok);
}
}
请注意,Python 源代码经过 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+