Pythonical проверить, является ли имя переменной допустимым
TL;DR; см. окончательную строку; остальное просто преамбула.
Я разрабатываю тестовый жгут, который анализирует пользовательские скрипты и генерирует Python script, который он запускает. Идея заключается в том, чтобы нетехнические люди могли писать сценарии высокого уровня тестирования.
Я ввел идею переменных, поэтому пользователь может использовать ключевое слово LET
в своем script. Например. LET X = 42
, который я просто расширяю до X = 42
. Затем они могут использовать X позже в своих скриптах - RELEASE CONNECTION X
Но что, если кто-то пишет LET 2 = 3
? Это приведет к созданию недопустимого Python.
Если у меня есть X
в переменной variableName
, то как я могу проверить, является ли variableName
допустимой переменной Python?
Ответы
Ответ 1
В Python 3 вы можете использовать str.isidentifier()
, чтобы проверить, является ли данная строка допустимым идентификатором/именем Python.
>>> 'X'.isidentifier()
True
>>> 'X123'.isidentifier()
True
>>> '2'.isidentifier()
False
>>> 'while'.isidentifier()
True
Последний пример показывает, что вы также должны проверить, столкнулось ли имя переменной с ключевым словом Python:
>>> from keyword import iskeyword
>>> iskeyword('X')
False
>>> iskeyword('while')
True
Итак, вы можете связать это в функции:
from keyword import iskeyword
def is_valid_variable_name(name):
return name.isidentifier() and not iskeyword(name)
Другой вариант, который работает в Python 2 и 3, заключается в использовании модуля ast
:
from ast import parse
def is_valid_variable_name(name):
try:
parse('{} = None'.format(name))
return True
except SyntaxError, ValueError, TypeError:
return False
>>> is_valid_variable_name('X')
True
>>> is_valid_variable_name('123')
False
>>> is_valid_variable_name('for')
False
>>> is_valid_variable_name('')
False
>>> is_valid_variable_name(42)
False
Это будет обрабатывать оператор присваивания без его фактического выполнения. Он будет получать недопустимые идентификаторы, а также попытки назначить ключевое слово. В приведенном выше коде None
- произвольное значение для присвоения данному имени - это может быть любое допустимое выражение для RHS.
Ответ 2
Вы можете использовать обработку исключений и на самом деле NameError
и SyntaxError
. Проверьте его внутри блока try/except
и сообщите пользователю, есть ли недопустимый ввод.
Ответ 3
Вы можете попробовать тестовое задание и посмотреть, вызывает ли он SyntaxError
:
>>> 2fg = 5
File "<stdin>", line 1
2fg = 5
^
SyntaxError: invalid syntax
Ответ 4
В Python 3, как и выше, вы можете просто использовать str.isidentifier
. Но в Python 2 этого не существует.
Модуль tokenize
имеет регулярное выражение для имен (идентификаторов): tokenize.Name
. Но я не мог найти никакой документации для этого, поэтому он может быть недоступен повсюду. Это просто r'[a-zA-Z_]\w*'
. Один $
после того, как вы проверите строки с re.match
.
docs говорят, что идентификатор определяется этой грамматикой:
identifier ::= (letter|"_") (letter | digit | "_")*
letter ::= lowercase | uppercase
lowercase ::= "a"..."z"
uppercase ::= "A"..."Z"
digit ::= "0"..."9"
Что эквивалентно регулярному выражению выше. Но мы должны импортировать tokenize.Name
, если это когда-либо изменится. (Что очень маловероятно, но, возможно, в более старых версиях Python это было другое?)
И чтобы отфильтровать ключевые слова, например pass
, def
и return
, используйте keyword.iskeyword
. Существует одно предостережение: None
не является ключевым словом в Python 2, но ему еще нельзя назначить. (keyword.iskeyword('None')
в Python 2 есть False
).
Итак:
import keyword
if hasattr(str, 'isidentifier'):
_isidentifier = str.isidentifier
else:
import re
_fallback_pattern = '[a-zA-Z_][a-zA-Z0-9_]*'
try:
import tokenize
except ImportError:
_isidentifier = re.compile(_fallback_pattern + '$').match
else:
_isidentifier = re.compile(
getattr(tokenize, 'Name', _fallback_pattern) + '$'
).match
del _fallback_pattern
def isname(s):
return bool(_isidentifier(s)) and not keyword.iskeyword(s) and s != 'None'
Ответ 5
Вы можете просто позволить Python (работает на любой используемой сегодня версии, насколько я знаю) проверит вас так, как обычно, внутренне, и поймает исключение:
def _dummy_function_taking_kwargs(**_):
pass
try:
_dummy_function_taking_kwargs(**{my_variable: None})
# if the above line didn't raise and we get here,
# the keyword/variable name was valid.
# You could also replace the external dummy function
# with an inline lambda function.
except TypeError:
# If we get here, it wasn't.
Примечательно, что TypeError
последовательно возникает всякий раз, когда dict
подвергается расширению аргумента ключевого слова и имеет ключ, который не является допустимым аргументом функции, и всякий раз, когда строит литерал dict
с недопустимым ключом.
Преимущество над принятым ответом состоит в том, что он и совместим как с Python 3, так и с 2, и не столь хрупким, как подход ast.parse
/compile
(который будет считать строки как foo = bar; qux
действительными).
Я не тщательно проверил это решение или написал тесты гипотезы, чтобы он его пугал, поэтому может быть какой-то угловой случай, но он, как правило, работает на Python 3.7, 3.6, 2.7 и 2.5 (не то, что кто-то должен чтобы использовать 2.5 в настоящее время, но он все еще находится в дикой природе, и вы можете быть одним из немногих бедных дернов, застрявших в написании кода, который работает с 2.6/2.5).
Ответ 6
Я не думаю, что вам нужен такой же синтаксис именования, что и сам python.
Скорее всего, для простого регулярного выражения, например:
\w+
чтобы убедиться, что это что-то буквенно-цифровое, а затем добавьте префикс, чтобы держаться подальше от собственного синтаксиса python. Таким образом, декларация пользователя, не относящаяся к технологиям:
LET return = 12
должен, вероятно, стать после разбора:
userspace_return = 12
or
userspace['return'] = 12