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