[英]Why does Python's grammar specification not include docstrings and comments?
我正在咨詢Python 3.6的官方Python語法規范 。
我無法找到任何注釋語法(它們顯示在#
前面)和文檔字符串(它們應顯示為'''
)。 快速查看詞法分析頁面也沒有幫助 - 文檔字符串在那里定義為長longstrings
但不出現在語法規范中。 名為STRING
的類型會進一步顯示,但不會引用其定義。
鑒於此,我很好奇CPython編譯器如何知道注釋和文檔字符串是什么。 這項壯舉是如何完成的?
我最初猜測CPython編譯器在第一次傳遞中刪除了注釋和文檔字符串,但隨后乞求了help()
如何呈現相關文檔字符串的問題。
docstring不是一個單獨的語法實體。 它只是一個常規的simple_stmt
(遵循該規則一直到atom
和STRING+
* 。如果它是函數體,類或模塊中的第一個語句,那么它將被編譯器用作 docstring。
[3]作為函數體中第一個語句出現的字符串文字被轉換為函數的
__doc__
屬性,因此轉換為函數的docstring。[4]作為類體中第一個語句出現的字符串文字被轉換為命名空間的
__doc__
項,因此轉換為類的docstring。
目前沒有為模塊指定相同的參考文檔,我認為這是一個文檔錯誤。
標記器刪除注釋,永遠不需要將其解析為語法。 他們的全部意義是在語法層面沒有意義。 請參閱Lexical Analysis文檔的“ 注釋”部分 :
注釋以不是字符串文字的一部分的哈希字符(#)開頭,並以物理行的末尾結束。 注釋表示邏輯行的結束,除非調用隱式行連接規則。 語法忽略注釋; 他們不是代幣 。
大膽強調我的。 因此, 標記生成器完全跳過評論:
/* Skip comment */
if (c == '#') {
while (c != EOF && c != '\n') {
c = tok_nextc(tok);
}
}
請注意,Python源代碼通過3個步驟:
語法僅適用於解析階段; 注釋將在tokenizer中刪除,docstrings僅對編譯器有用。
為了說明解析器如何不將文檔字符串視為字符串文字表達式以外的任何內容,您可以通過ast
模塊訪問任何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
對象,它作為正文中的第一個元素,是一個帶有值'docstring'
的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
因此,編譯后的代碼生成了模塊的頂級語句。 MAKE_FUNCTION
操作碼使用存儲的代碼對象(頂級代碼對象常量的一部分)來構建函數。 所以我們在索引0處查看嵌套代碼對象:
>>> dis.dis(codeobj.co_consts[0])
1 0 LOAD_CONST 1 (None)
2 RETURN_VALUE
這里的docstring似乎已經消失了 。 該函數只是返回None
。 而docstring則存儲為常量:
>>> codeobj.co_consts[0].co_consts
('docstring', None)
執行MAKE_FUNCTION
操作碼時,如果是字符串,則第一個常量變為函數對象的__doc__
屬性。
編譯完成后,我們可以使用exec()
函數將代碼對象exec()
到給定的命名空間中,該命名空間添加了一個帶有docstring的函數對象:
>>> namespace = {}
>>> exec(codeobj, namespace)
>>> namespace['foo']
<function foo at 0x107c23e18>
>>> namespace['foo'].__doc__
'docstring'
所以編譯器的工作就是確定什么是文檔字符串。 這是在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;
}
這是從文檔字符串有意義的位置調用的; 對於模塊和類,在compiler_body()
,對於函數,在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+
在標記化/詞法分析期間,將忽略注釋(以#
開頭的任何內容),因此無需編寫規則來解析它們。 它們不向解釋器/編譯器提供任何語義信息,因為它們僅用於為讀者提高程序的詳細程度,因此它們被忽略。
這是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答案的補充。 這並不意味着復制他在帖子中提供的任何信息。 現在,據說,......
我最初猜測CPython編譯器在第一遍中刪除了注釋和文檔字符串[...]
Docstrings(未分配給任何變量名的字符串文字, '...'
, "..."
, '''...'''
或"""..."""
)確實是處理。 正如Martijn Pieters在他的回答中提到的那樣,它們被解析為簡單的字符串文字( STRING+
令牌)。 從當前的文檔開始,只是順便提一下,文檔字符串被賦值給函數/ class / module的__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
如您所見,字節代碼中沒有提到我們的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,)
docstrings現在已經消失了。
-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 """
已被忽略。 您會感興趣的是,此刪除操作是字節代碼基本優化的一部分。
(你可能會發現這些引用和我一樣有趣。)
dis
模塊
peephole.c
(Martijn提供) - 所有編譯器優化的源代碼。 如果您能理解它,這尤其令人着迷!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.