验证一个字符串是否为有效的Python标识符的正则表达式是什么?

17

我有以下对标识符的定义:

Identifier --> letter{ letter| digit}
基本上我有一个标识符函数,它从文件中获取一个字符串并测试它以确保它是一个有效的标识符,如上所定义。 我尝试过这个:
if re.match('\w+(\w\d)?', i):     
  return True
else:
  return False

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

例如:

c = 0 ;

它打印出c作为一个有效的标识符,这很好,但它也打印出0作为一个有效的标识符。

我在这里做错了什么?


2
你知道你的定义和Python的不一样,对吧?Python也允许使用下划线。 - Tom Zych
所有的正则表达式答案都不太正确,请参见下面 - Hatshepsut
7个回答

29
问题是在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建议的那样,并通过限制为仅使用ASCII标识符来解决另一个问题,但完全没有必要使用正则表达式。
正如Hatshepsut所说:

str.isidentifier()就可以解决问题

只需使用它即可解决问题。

如问题所要求的那样,我的原始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" ]
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

6
可能值得一提的是,这与关键字如 Truereturn 等匹配。我并不建议更改正则表达式,只是希望原帖作者记在心里。 - JoeCondron
2
@JoeCondron 这也非常容易实现,因为Python包含keyword.iskeyword函数,它只是一个围绕关键字列表frozenset的包装器。 - Alex Huszagh
2
至少在 Python 3.6 中,即使'℘᧚'是 Python 3 中有效的标识符(且不是关键字),这也不能正常工作。 - martineau

14

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

这都在哪里记录?

它在哪里实现了?

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

我仍然需要一个正则表达式

看一下 PyPI 上的 regex 模块。

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

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


你能提供一个例子,说明在这种情况下它可以工作,但正则表达式失败了吗? - martineau
确实,你是对的——但这让我感到惊讶,因为re文档似乎表明它支持Unicode字符串(即使在Python 3.x中没有re.UNICODE标志)。 - martineau
@martineau 顺便问一下,你是怎么找到那个特定的东西的? - Hatshepsut
我正在开发一个正则表达式来识别Python的“特殊”方法名称,即以两个下划线字符开头和结尾的名称。__ - 也称为“dunder”名称,因此在搜索这个网站时寻找一个通用的方法来识别任何有效的标识符。猜测,我将不得不放弃使用re正则表达式来完成它...实际上,我现在怀疑该模块的限制/错误可能是为什么在Python 3中添加了字符串isidentifier方法的原因。 - martineau
继您最近的更新之后:是的,我知道有第三方正则表达式库,但我更喜欢将我的工作限制在标准库中 - 因此将使用str.isidentifer()str.startswith()str.endswith()的组合来检测dunder名称(代码并不特别关注速度)。感谢您的回复(和答案更新)。 - martineau
多么惊人的研究和答案啊!让我完全改变了自己的看法。我不知道 str.isidentifier(),不确定它在2012年是否可用。但在2021年,使用正则表达式真的毫无意义。 - MestreLion

3

对于Python 3,您需要处理Unicode字母和数字。因此,如果这是一个问题,您应该采取以下措施:

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

[^\d\W] 匹配一个既不是数字也不是“非字母数字”的字符,这可以翻译为“一个字母或下划线字符”。


2
差不多了...但还不够...它会对单个字母标识符"a"失败,并且还允许"aa@#$%"作为有效的标识符。 - MestreLion

2

\w 可以匹配数字和字母。试试使用^[_a-zA-Z]\w*$


5
注意,Python 3允许在标识符中使用所有的Unicode字母和数字。 - Tim Pietzcker
应该使用“[_a-zA-Z]\w*”吗?因为你想匹配初始字符后的0个或多个字符。 - William Stein
1
这将把"a$%@#%"作为一个有效的标识符匹配。 - MestreLion

0

这个问题涉及到正则表达式,所以我的答案可能看起来与主题不符。关键是正则表达式根本不是正确的方法。

想了解有问题的字符吗?

使用 str.isidentifier,可以逐个字符执行检查,并在前缀中添加下划线,以避免诸如数字等误报情况...如果其(带前缀)组件之一无效,那么名称怎么能有效呢?例如:

def checker(str_: str) -> 'set[str]':
    return {
        c for i, c in enumerate(str_)
        if not (f'_{c}' if i else c).isidentifier()
    }

>>> checker('℘3᧚₂')
{'₂'}

哪种解决方案可以处理未经授权的首字符,例如数字或。请参见

>>> checker('᧚℘3₂')
{'₂', '᧚'}
>>> checker('3᧚℘₂')
{'3', '₂'}
>>> checker("a$%@#%\n")
{'@', '#', '\n', '$', '%'}

需要改进,因为它既不检查保留名称,也不说明为什么有时会出问题,而则总是会出问题...但这里是我的非正则表达式方法。

以你的角度来看,我的回答是:

if not checker(i):
    return True
else:
    return False

这可以被简化为

return not checker(i)

0

我需要一个有效的正则表达式(即我不能只使用str.isidentifier),因为我需要在字符串中找到所有嵌入的标识符,而不仅仅是测试整个字符串是否是有效的标识符。我也不能使用ast模块,因为我预期该字符串不是有效的Python语法。所以现有的答案没有帮助,我对“使用正则表达式包”也不满意。所以这里是一个真实的正则表达式来完成这个任务,以及构造它和测试它的代码。

# coding: utf-8
import itertools
import re

full_pattern = r"[A-Z_a-zªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶ-ͷͻ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙՠ-ֈא-תׯ-ײؠ-يٮ-ٯٱ-ۓەۥ-ۦۮ-ۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴ-ߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࡠ-ࡪࢠ-ࢴࢶ-ࣇऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএ-ঐও-নপ-রলশ-হঽৎড়-ঢ়য়-ৡৰ-ৱৼਅ-ਊਏ-ਐਓ-ਨਪ-ਰਲ-ਲ਼ਵ-ਸ਼ਸ-ਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલ-ળવ-હઽૐૠ-ૡૹଅ-ଌଏ-ଐଓ-ନପ-ରଲ-ଳଵ-ହଽଡ଼-ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கங-சஜஞ-டண-தந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౠ-ౡಀಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠ-ೡೱ-ೲഄ-ഌഎ-ഐഒ-ഺഽൎൔ-ൖൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาเ-ๆກ-ຂຄຆ-ຊຌ-ຣລວ-ະາຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥ-ၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡸᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮ-ᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᲀ-ᲈᲐ-ᲺᲽ-Ჿᳩ-ᳬᳮ-ᳳᳵ-ᳶᳺᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕ℘-ℝℤΩℨK-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲ-ⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ々-〇〡-〩〱-〵〸-〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄯㄱ-ㆎㆠ-ㆿㇰ-ㇿ㐀-䶿一-鿼ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪ-ꘫꙀ-ꙮꙿ-ꚝꚠ-ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞿꟂ-ꟊꟵ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽ-ꣾꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵ-ꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭩꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﱝﱤ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷹﹱﹳﹷﹹﹻﹽﹿ-ﻼA-Za-zヲ-ンᅠ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------][0-9A-Z_a-zªµ·ºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮ̀-ʹͶ-ͷͻ-ͽͿΆ-ΊΌΎ-ΡΣ-ϵϷ-ҁ҃-҇Ҋ-ԯԱ-Ֆՙՠ-ֈ֑-ֽֿׁ-ׂׄ-ׇׅא-תׯ-ײؐ-ؚؠ-٩ٮ-ۓە-ۜ۟-۪ۨ-ۼۿܐ-݊ݍ-ޱ߀-ߵߺ߽ࠀ-࠭ࡀ-࡛ࡠ-ࡪࢠ-ࢴࢶ-ࣇ࣓-ࣣ࣡-ॣ०-९ॱ-ঃঅ-ঌএ-ঐও-নপ-রলশ-হ়-ৄে-ৈো-ৎৗড়-ঢ়য়-ৣ০-ৱৼ৾ਁ-ਃਅ-ਊਏ-ਐਓ-ਨਪ-ਰਲ-ਲ਼ਵ-ਸ਼ਸ-ਹ਼ਾ-ੂੇ-ੈੋ-੍ੑਖ਼-ੜਫ਼੦-ੵઁ-ઃઅ-ઍએ-ઑઓ-નપ-રલ-ળવ-હ઼-ૅે-ૉો-્ૐૠ-ૣ૦-૯ૹ-૿ଁ-ଃଅ-ଌଏ-ଐଓ-ନପ-ରଲ-ଳଵ-ହ଼-ୄେ-ୈୋ-୍୕-ୗଡ଼-ଢ଼ୟ-ୣ୦-୯ୱஂ-ஃஅ-ஊஎ-ஐஒ-கங-சஜஞ-டண-தந-பம-ஹா-ூெ-ைொ-்ௐௗ௦-௯ఀ-ఌఎ-ఐఒ-నప-హఽ-ౄె-ైొ-్ౕ-ౖౘ-ౚౠ-ౣ౦-౯ಀ-ಃಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹ಼-ೄೆ-ೈೊ-್ೕ-ೖೞೠ-ೣ೦-೯ೱ-ೲഀ-ഌഎ-ഐഒ-ൄെ-ൈൊ-ൎൔ-ൗൟ-ൣ൦-൯ൺ-ൿඁ-ඃඅ-ඖක-නඳ-රලව-ෆ්ා-ුූෘ-ෟ෦-෯ෲ-ෳก-ฺเ-๎๐-๙ກ-ຂຄຆ-ຊຌ-ຣລວ-ຽເ-ໄໆ່-ໍ໐-໙ໜ-ໟༀ༘-༙༠-༩༹༵༷༾-ཇཉ-ཬཱ-྄྆-ྗྙ-ྼ࿆က-၉ၐ-ႝႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚ፝-፟፩-፱ᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛸᜀ-ᜌᜎ-᜔ᜠ-᜴ᝀ-ᝓᝠ-ᝬᝮ-ᝰᝲ-ᝳក-៓ៗៜ-៝០-៩᠋-᠍᠐-᠙ᠠ-ᡸᢀ-ᢪᢰ-ᣵᤀ-ᤞᤠ-ᤫᤰ-᤻᥆-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉ᧐-᧚ᨀ-ᨛᨠ-ᩞ᩠-᩿᩼-᪉᪐-᪙ᪧ᪰-᪽ᪿ-ᫀᬀ-ᭋ᭐-᭙᭫-᭳ᮀ-᯳ᰀ-᰷᱀-᱉ᱍ-ᱽᲀ-ᲈᲐ-ᲺᲽ-Ჿ᳐-᳔᳒-ᳺᴀ-᷹᷻-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼ‿-⁀⁔ⁱⁿₐ-ₜ⃐-⃥⃜⃡-⃰ℂℇℊ-ℓℕ℘-ℝℤΩℨK-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯ⵿-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⷠ-ⷿ々-〇〡-〯〱-〵〸-〼ぁ-ゖ゙-゚ゝ-ゟァ-ヺー-ヿㄅ-ㄯㄱ-ㆎㆠ-ㆿㇰ-ㇿ㐀-䶿一-鿼ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘫꙀ-꙯ꙴ-꙽ꙿ-꛱ꜗ-ꜟꜢ-ꞈꞋ-ꞿꟂ-ꟊꟵ-ꠧ꠬ꡀ-ꡳꢀ-ꣅ꣐-꣙꣠-ꣷꣻꣽ-꤭ꤰ-꥓ꥠ-ꥼꦀ-꧀ꧏ-꧙ꧠ-ꧾꨀ-ꨶꩀ-ꩍ꩐-꩙ꩠ-ꩶꩺ-ꫂꫛ-ꫝꫠ-ꫯꫲ-꫶ꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭩꭰ-ꯪ꯬-꯭꯰-꯹가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִ-ﬨשׁ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﱝﱤ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷹ︀-️︠-︯︳-︴﹍-﹏ﹱﹳﹷﹹﹻﹽﹿ-ﻼ0-9A-Z_a-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------]*"


def chars():
    for i in itertools.count():
        try:
            yield chr(i)
        except ValueError:
            break


def make_full_pattern():
    def make_pattern(is_valid):
        pattern = ""

        for is_identifier, group in itertools.groupby(chars(), is_valid):
            if is_identifier:
                group = list(group)
                if len(group) == 1:
                    pattern += group[0]
                else:
                    pattern += group[0] + "-" + group[-1]

        return "[" + pattern + "]"

    return make_pattern(str.isidentifier) + make_pattern(lambda c: ("x" + c).isidentifier()) + "*"


def test_pattern():
    assert full_pattern == make_full_pattern()
    identifier_regex = re.compile(full_pattern)

    for char in chars():
        for string in [char, "x" + char]:
            assert bool(identifier_regex.fullmatch(string)) == string.isidentifier()


test_pattern()

-1

非常好用:r'[^\d\W][\w\d]+'


不匹配单个字符,还有一堆其他遗漏,如Unicode。 - lionello

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接