确认字符串是否为有效Python标识符的正则表达式?
我有一个关于标识符的定义:
Identifier --> letter{ letter| digit}
简单来说,我有一个标识符的函数,它会从文件中读取一个字符串,并检查这个字符串是否符合上面定义的有效标识符。
我尝试过这个:
if re.match('\w+(\w\d)?', i):
return True
else:
return False
但是每次我的程序遇到一个整数时,它都会认为这个整数是一个有效的标识符。
举个例子:
c = 0 ;
它会把 c
识别为有效标识符,这没问题,但它也会把 0
识别为有效标识符。
我这里做错了什么呢?
6 个回答
对于Python 3,你需要处理Unicode字母和数字。所以如果你在这方面有疑虑,可以参考以下内容:
re_ident = re.compile(r"^[^\d\W]\w*$", re.UNICODE)
[^\d\W]
这个表达式的意思是匹配一个既不是数字也不是“非字母数字”的字符,也就是说,它匹配的是字母或下划线。
str.isidentifier()
这个方法是有效的。使用正则表达式的答案有时会错误地匹配一些有效的 Python 标识符,同时也会错误地匹配一些无效的标识符。
str.isidentifier()
如果字符串是根据语言定义的有效标识符,就返回真。可以使用
keyword.iskeyword()
来测试一些保留的标识符,比如 def 和 class。
@martineau 的评论中提到一个例子 '℘᧚'
,在这里正则表达式的解决方案就失败了。
>>> '℘᧚'.isidentifier()
True
>>> import re
>>> bool(re.search(r'^[^\d\W]\w*\Z', '℘᧚'))
False
为什么会这样呢?
我们来定义一下与给定正则表达式匹配的代码点集合,以及与 str.isidentifier
匹配的集合。
import re
import unicodedata
chars = {chr(i) for i in range(0x10ffff) if re.fullmatch(r'^[^\d\W]\w*\Z', chr(i))}
identifiers = {chr(i) for i in range(0x10ffff) if chr(i).isidentifier()}
有多少正则表达式匹配的结果不是标识符?
In [26]: len(chars - identifiers)
Out[26]: 698
有多少标识符不是正则表达式匹配的结果?
In [27]: len(identifiers - chars)
Out[27]: 4
有趣——哪些呢?
In [37]: {(c, unicodedata.name(c), unicodedata.category(c)) for c in identifiers - chars}
Out[37]:
set([
('\u1885', 'MONGOLIAN LETTER ALI GALI BALUDA', 'Mn'),
('\u1886', 'MONGOLIAN LETTER ALI GALI THREE BALUDA', 'Mn'),
('℘', 'SCRIPT CAPITAL P', 'Sm'),
('℮', 'ESTIMATED SYMBOL', 'So'),
])
这两个集合有什么不同?
它们的 Unicode “通用类别”值不同。
In [31]: {unicodedata.category(c) for c in chars - identifiers}
Out[31]: set(['Lm', 'Lo', 'No'])
根据 维基百科,这些值包括 Letter, modifier
;Letter, other
;Number, other
。这与 re 文档 是一致的,因为 \d
只匹配十进制数字:
\d
匹配任何 Unicode 十进制数字(也就是说,任何属于 Unicode 字符类别 [Nd] 的字符)
In [32]: {unicodedata.category(c) for c in identifiers - chars}
Out[32]: set(['Mn', 'Sm', 'So'])
那是 Mark, nonspacing
;Symbol, math
;Symbol, other
。
这些内容在哪里有文档记录?
- 在 Python 语言参考 中
- 在 PEP 3131 - 支持非 ASCII 标识符 中
这些内容是在哪里实现的?
https://github.com/python/cpython/commit/47383403a0a11259acb640406a8efc38981d2255
我还是想要一个正则表达式
可以查看 regex 模块在 PyPI 上的内容。
这个正则表达式的实现与标准的 ‘re’ 模块向后兼容,但提供了额外的功能。
它包括“通用类别”的过滤器。
这个问题是在10年前提出的,那时候Python 2还很流行。过去十年的很多评论表明,我的回答需要大幅更新,首先要提醒大家:
没有简单的正则表达式能够正确匹配所有(且仅仅是)有效的Python标识符。Python 2时不行,Python 3也不行。
原因有:
正如JoeCondron所指出的,Python中的保留关键字,比如
True
、if
、return
,是不有效的标识符,因此单靠简单的正则表达式无法处理这些情况,还需要额外的过滤。Python 3允许在标识符中使用非ASCII字母和数字,但有效标识符的词法解析器接受的字母和数字的Unicode类别与在
re
模块中的\d
、\w
、\W
类别并不匹配,martineau的反例和Hatshepsut的精彩研究对此进行了详细解释。
我们可以尝试解决第一个问题,方法是使用keyword.iskeyword()
,正如Alexander Huszagh所建议的,或者像Feuermurmel所提到的那样,在一个(巨大的)正则表达式的负向前瞻中列出所有关键字。对于另一个问题,我们可以通过限制只使用ASCII标识符来解决。
但是,考虑到这些麻烦和限制,为什么还要使用正则表达式呢?
正如Hatshepsut所说:
str.isidentifier()
是有效的
直接使用它,问题就解决了。
PS:如果你只是因为这个而给我点赞,请也给最初发布这个解决方案的回答点赞!
根据问题的要求,我2012年的原始回答提供了一个基于Python 2官方标识符定义的正则表达式:
identifier ::= (letter|"_") (letter | digit | "_")*
可以用以下正则表达式表示:
^[^\d\W]\w*\Z
示例:
import re
identifier = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE)
tests = [ "a", "a1", "_a1", "1a", "aa$%@%", "aa bb", "aa_bb", "aa\n", "if" ]
for test in tests:
result = re.match(identifier, test)
print("%r\t= %s" % (test, (result is not None)))
结果:
'a' = True
'a1' = True
'_a1' = True
'1a' = False
'aa$%@%' = False
'aa bb' = False
'aa_bb' = True
'aa\n' = False
'if' = True
记得使用keyword.iskeyword()
来避免像最后一个这样的误判。