我正在咨询官方Python grammar specification as of Python 3.6。
我无法找到任何评论语法(它们显示在#
前面)和文档字符串(它们应显示为'''
)。快速查看the lexical analysis页面也没有任何帮助 - 文档字符串在那里定义为longstrings
但未出现在语法规范中。名为STRING
的类型会进一步显示,但不会引用其定义。
鉴于此,我很好奇CPython编译器如何知道注释和文档字符串是什么。这项壮举是如何完成的?
我最初猜测CPython编译器在第一次传递中删除了注释和文档字符串,但随后又乞求了help()
如何呈现相关文档字符串的问题。
答案 0 :(得分:7)
docstring不是一个单独的语法实体。它只是一个常规simple_stmt
(遵循该规则一直到atom
和STRING+
* 。如果它是第一个函数体,类或模块中的语句,然后编译器使用作为文档字符串。
[3]作为函数体中第一个语句出现的字符串文字被转换为函数的
__doc__
属性,因此转换为函数的文档字符串。[4]作为类体中第一个语句出现的字符串文字被转换为命名空间的
__doc__
项,因此转换为类的文档字符串。
目前没有为模块指定相同的参考文档,我认为这是一个文档错误。
标记器删除了注释,永远不需要将其解析为语法。他们的整个点在语法层面上没有意义。请参阅Lexical Analysis文档的Comments section:
注释以不是字符串文字的一部分的哈希字符(#)开头,并以物理行的末尾结束。注释表示逻辑行的结束,除非调用隐式行连接规则。 语法忽略注释;他们不是代币。
大胆强调我的。所以tokenizer完全跳过了评论:
/* Skip comment */
if (c == '#') {
while (c != EOF && c != '\n') {
c = tok_nextc(tok);
}
}
请注意,Python源代码经过3个步骤:
语法仅适用于解析阶段;注释将在tokenizer中删除,docstrings只对编译器有用。
为了说明解析器不将文档字符串视为字符串文字表达式以外的任何内容,您可以通过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'
的表达式。然后,编译器生成一个代码对象,将该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
。而docstring则存储为常量:
>>> codeobj.co_consts[0].co_consts
('docstring', None)
执行MAKE_FUNCTION
操作码时,如果它是一个字符串,则第一个常量变为函数对象的__doc__
属性。
编译完成后,我们可以使用exec()
function将代码对象执行到给定的命名空间中,这会添加一个带有docstring的函数对象:
>>> 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 :注释不是语法的一部分,因为语法分析器甚至看不到注释。标记生成器会跳过它们。 Docstrings不是语法的一部分,因为对于语法分析器,它们只是字符串文字。编译步骤(采用解析器的解析树输出)将这些字符串表达式解释为docstrings。
* 完整的语法规则路径为simple_stmt
- &gt; small_stmt
- &gt; expr_stmt
- &gt; testlist_star_expr
- &gt; star_expr
- &gt; expr
- &gt; xor_expr
- &gt; and_expr
- &gt; shift_expr
- &gt; arith_expr
- &gt; term
- &gt; factor
- &gt; power
- &gt; atom_expr
- &gt; atom
- &gt; STRING+
答案 1 :(得分:6)
在标记化/词法分析期间,将忽略注释(以#
开头的任何内容),因此无需编写规则来解析它们。它们不向解释器/编译器提供任何语义信息,因为它们仅用于为读者提高程序的详细程度,因此它们被忽略。
这是ANSI C编程语言的lex规范:http://www.quut.com/c/ANSI-C-grammar-l-1998.html。我想提请你注意这里处理评论的方式:
"/*" { 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
语句结束,这意味着它是一个有效的标记,必须进行解析。
现在,这里是处理评论的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完全相同。
注意:我的答案的这一部分是对@ MartijnPieters&#39;的补充。回答。这并不意味着复制他在帖子中提供的任何信息。现在,据说,......
我最初猜测评论和文档字符串被删除了 首先通过CPython编译器[...]
文档字符串(未分配给任何变量名称的字符串文字,'...'
,"..."
,'''...'''
或"""..."""
中的任何内容)确实已处理完毕。它们被解析为简单的字符串文字(STRING+
标记),正如Martijn Pieters在his answer中提到的那样。从当前的文档开始,只是顺便提一下,文档字符串被赋值给函数/ class / module&lt; __doc__
属性。如何做到并没有在任何地方深入提及。
实际发生的是它们被标记化并解析为字符串文字,生成的结果解析树将包含它们。从解析树生成字节代码,文档字符串位于__doc__
属性中的正确位置(它们不是字节代码的明确部分,如下所示)。我不会详细介绍,因为我上面链接的答案描述的内容非常详细。
当然,可以完全忽略它们。如果您使用python -OO
(-OO
标志代表&#34;强烈优化&#34;,而不是-O
代表&#34;温和优化&#34;),将生成的字节代码存储在.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
正如您所看到的,字节代码中没有提到我们的docstring。但是,他们在那里。要获得文档字符串,您可以执行...
>>> code.co_consts[0].co_consts
(' docstring ', None)
因此,正如您所看到的,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
不,差异,但......
>>> code.co_consts[0].co_consts
(None,)
文档字符串现在已经消失了。
-O
和-OO
标志只删除内容(默认情况下优化字节代码... -O
从生成的字节码中删除断言语句和if __debug__:
套件,而-OO
除了忽略文档字符串之外)。结果编译时间会略有减少。此外,执行速度保持不变,除非您有大量assert
和if __debug__:
语句,否则对性能没有影响。
另外,请记住只有在文档字符串是函数/类/模块定义中的第一个内容时才会保留文档字符串。编译期间只删除所有其他字符串。如果您将test.py
更改为以下内容:
def foo():
""" docstring """
"""test"""
pass
然后用optimization=0
重复相同的过程,这在编译时存储在co_consts
变量中:
>>> code.co_consts[0].co_consts
(' docstring ', None)
意思是""" test """
被忽略了。您会感兴趣的是,此删除操作是字节代码基本优化的一部分。
(你可能会发现这些引用和我一样有趣。)