解析Python标识符
我需要判断一个字符串是否是有效的 Python 标识符。因为 Python 3 的标识符支持一些比较复杂的 Unicode 功能,而且 Python 的语法可能会随着版本更新而变化,所以我决定不手动解析。可惜的是,我尝试使用 Python 的内部接口,但似乎没有成功:
I. 函数 compile
>>> string = "a = 5; b "
>>> test = "{} = 5"
>>> compile(test.format(string), "<string>", "exec")
<code object <module> at 0xb71b4d90, file "<string>", line 1>
很明显,test
不能强制 compile 使用 ast.Name 作为抽象语法树(AST)的根节点。
接下来,我尝试使用 ast
和 parser
模块。这些模块是用来生成字符串的,而不是判断一个字符串是否符合某种生成规则,但我想它们可能还是有用的。
II. 模块 ast
>>> a=ast.Module(body=[ast.Expr(value=ast.Name(id='1a', ctx=ast.Load()))])
>>> af = ast.fix_missing_locations(a)
>>> c = compile(af, "<string>", "exec")
>>> exec(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name '1a' is not defined
好吧,很明显 Name 并没有正确解析 '1a'。也许这个步骤应该在解析阶段更早的时候进行。
III. 模块 parser
>>> p = parser.suite("a")
>>> t = parser.st2tuple(p)
>>> t
(257, (268, (269, (270, (271, (272, (302, (306, (307, (308, (309, (312, (313, (314, (315, (316, (317, (318, (319, (320, (1, 'a')))))))))))))))))), (4, ''))), (4, ''), (0, ''))
>>>
>>> t = (257, (268, (269, (270, (271, (272, (302, (306, (307, (308, (309, (312, (313, (314, (315, (316, (317, (318, (319, (320, (1, '1a')))))))))))))))))), (4, ''))), (4, ''), (0, ''))
>>> p = parser.sequence2st(t)
>>> c = parser.compilest(p)
>>> exec(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<syntax-tree>", line 0, in <module>
NameError: name '1a' is not defined
好的,还是没有被检查……为什么呢?快速查看一下 Python 的 完整语法规范,发现 NAME 并没有被定义。如果这些检查是由字节码编译器执行的,难道 1a
不应该被抓住吗?
我开始怀疑 Python 并没有提供任何功能来实现这个目标。同时,我也很好奇为什么有些尝试会失败。
2 个回答
我不太确定你提到的 compile
示例想表达什么,但如果你 compile
仅仅是为了潜在的标识符 eval
,那么这就揭示了事情的真相。
>>> dis(compile("1", "<string>", "eval"))
1 0 LOAD_CONST 0 (1)
3 RETURN_VALUE
>>> dis(compile("a", "<string>", "eval"))
1 0 LOAD_NAME 0 (a)
3 RETURN_VALUE
>>> dis(compile("1a", "<string>", "eval"))
File "<string>", line 1
1a
^
SyntaxError: unexpected EOF while parsing
>>> dis(compile("你好", "<string>", "eval"))
1 0 LOAD_NAME 0 (你好)
3 RETURN_VALUE
在真正使用之前,需要进行更多的测试(特别是针对一些特殊情况),不过得到一个 LOAD_NAME
的操作码是个好兆头。失败的情况可能包括出现异常或者得到不同的操作码,所以你需要同时检查这两种情况。
你不需要去解析,只需要进行 词法分析,如果你在意的话,可以检查一下返回的 NAME
是否是关键字。
下面是一个例子,部分内容来自于链接的文档:
>>> import tokenize
>>> from io import BytesIO
>>> from keyword import iskeyword
>>> s = "def twoπ(a,b):"
>>> g = tokenize.tokenize(BytesIO(s.encode("utf-8")).readline)
>>> for toktype, tokval, st, end, _ in g:
... if toktype == tokenize.NAME and iskeyword(tokval):
... print ("KEYWORD ", tokval)
... else:
... print(toktype, tokval)
...
56 utf-8
KEYWORD def
1 twoπ
52 (
1 a
52 ,
1 b
52 )
52 :
0
在输入的开始,你总是会得到一个编码(ENCODING,编号56)的标记,而在最后会有一个结束标记(ENDMARKER,编号0)。