确认字符串是否为有效Python标识符的正则表达式?

19 投票
6 回答
21298 浏览
提问于 2025-04-16 14:39

我有一个关于标识符的定义:

Identifier --> letter{ letter| digit}

简单来说,我有一个标识符的函数,它会从文件中读取一个字符串,并检查这个字符串是否符合上面定义的有效标识符。

我尝试过这个:

if re.match('\w+(\w\d)?', i):     
  return True
else:
  return False

但是每次我的程序遇到一个整数时,它都会认为这个整数是一个有效的标识符。

举个例子:

c = 0 ;

它会把 c 识别为有效标识符,这没问题,但它也会把 0 识别为有效标识符。

我这里做错了什么呢?

6 个回答

3

对于Python 3,你需要处理Unicode字母和数字。所以如果你在这方面有疑虑,可以参考以下内容:

re_ident = re.compile(r"^[^\d\W]\w*$", re.UNICODE)

[^\d\W] 这个表达式的意思是匹配一个既不是数字也不是“非字母数字”的字符,也就是说,它匹配的是字母或下划线。

15

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, modifierLetter, otherNumber, other。这与 re 文档 是一致的,因为 \d 只匹配十进制数字:

\d 匹配任何 Unicode 十进制数字(也就是说,任何属于 Unicode 字符类别 [Nd] 的字符)

In [32]: {unicodedata.category(c) for c in identifiers - chars}                                                                                 
Out[32]: set(['Mn', 'Sm', 'So'])

那是 Mark, nonspacingSymbol, mathSymbol, other

这些内容在哪里有文档记录?

这些内容是在哪里实现的?

https://github.com/python/cpython/commit/47383403a0a11259acb640406a8efc38981d2255

我还是想要一个正则表达式

可以查看 regex 模块在 PyPI 上的内容。

这个正则表达式的实现与标准的 ‘re’ 模块向后兼容,但提供了额外的功能。

它包括“通用类别”的过滤器。

30

这个问题是在10年前提出的,那时候Python 2还很流行。过去十年的很多评论表明,我的回答需要大幅更新,首先要提醒大家:

没有简单的正则表达式能够正确匹配所有(且仅仅是)有效的Python标识符。Python 2时不行,Python 3也不行。

原因有:

  • 正如JoeCondron所指出的,Python中的保留关键字,比如Trueifreturn,是有效的标识符,因此单靠简单的正则表达式无法处理这些情况,还需要额外的过滤。

  • 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()来避免像最后一个这样的误判。

撰写回答