Compruebe pythonicamente si un nombre de variable es válido

tldr; ver la línea final; el rest es solo un preámbulo


Estoy desarrollando un arnés de prueba, que analiza los scripts de los usuarios y genera un script de Python que luego ejecuta. La idea es que las personas sin conocimientos técnicos puedan escribir scripts de prueba de alto nivel.

He introducido la idea de las variables, por lo que un usuario puede utilizar la palabra clave LET en su script. Por ejemplo, LET X = 42 , que simplemente amplío a X = 42 . Luego pueden usar X más adelante en sus scripts – RELEASE CONNECTION X

Pero, ¿y si alguien escribe LET 2 = 3 ? Eso va a generar Python inválido.

Si tengo esa X en una variable variableName , ¿cómo puedo verificar si variableName es una variable válida de Python?

En Python 3 puede usar str.isidentifier() para probar si una cadena dada es un identificador / nombre válido de Python.

 >>> 'X'.isidentifier() True >>> 'X123'.isidentifier() True >>> '2'.isidentifier() False >>> 'while'.isidentifier() True 

El último ejemplo muestra que también debe verificar si el nombre de la variable coincide con una palabra clave de Python:

 >>> from keyword import iskeyword >>> iskeyword('X') False >>> iskeyword('while') True 

Así que podrías poner eso juntos en una función:

 from keyword import iskeyword def is_valid_variable_name(name): return name.isidentifier() and not iskeyword(name) 

Otra opción, que funciona en Python 2 y 3, es usar el módulo 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 

Esto analizará la instrucción de asignación sin ejecutarlo realmente. Recogerá identificadores no válidos, así como los bashs de asignar a una palabra clave. En el código anterior, None es un valor arbitrario para asignar al nombre dado, podría ser cualquier expresión válida para el RHS.

Podría usar el manejo de excepciones y capturar realmente NameError y SyntaxError . Pruébelo dentro del bloque try/except e informe al usuario si hay alguna entrada no válida.

Puedes probar una asignación de prueba y ver si genera un SyntaxError :

 >>> 2fg = 5 File "", line 1 2fg = 5 ^ SyntaxError: invalid syntax 

En Python 3, como se str.isidentifier arriba, simplemente puede usar str.isidentifier . Pero en Python 2, esto no existe.

El módulo tokenize tiene una expresión regular para los nombres (identificadores): tokenize.Name . Pero no pude encontrar ninguna documentación para él, por lo que puede que no esté disponible en todas partes. Simplemente es r'[a-zA-Z_]\w*' . Un solo $ después le permitirá probar cadenas con re.match .

Los documentos dicen que un identificador está definido por esta gramática:

 identifier ::= (letter|"_") (letter | digit | "_")* letter ::= lowercase | uppercase lowercase ::= "a"..."z" uppercase ::= "A"..."Z" digit ::= "0"..."9" 

Lo que es equivalente a la expresión regular anterior. Pero aún debemos importar tokenize.Name en caso de que esto cambie. (Lo cual es muy poco probable, pero tal vez en versiones anteriores de Python fuera diferente)

Y para filtrar palabras clave, como pass , def y return , use keyword.iskeyword . Hay una advertencia: None no es una palabra clave en Python 2, pero aún no se puede asignar. ( keyword.iskeyword('None') en Python 2 es False ).

Asi que:

 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' 

Puede simplemente dejar que Python (funciona en cualquier versión en uso hoy en día, por lo que sé) haga la comprobación por usted de la forma en que normalmente lo haría internamente, y detecte la excepción:

 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. 

En particular, TypeError se TypeError constantemente cuando un dict sufre una expansión de argumento de palabra clave y tiene una clave que no es un argumento de función válida, y cada vez que se construye un dict literal con una clave no válida.

La ventaja sobre la respuesta aceptada es que es compatible tanto con Python 3 como con 2, y no tan frágil como el enfoque ast.parse / compile (que contaría cadenas como foo = bar; qux como válida).

No he auditado a fondo esta solución ni he escrito pruebas de hipótesis para que se vea borrosa, por lo que podría haber algún caso de esquina, pero parece que generalmente funciona en Python 3.7, 3.6, 2.7 y 2.5 (no es que alguien deba estar usando). 2.5 hoy en día, pero aún está en libertad y puede ser uno de los pocos pobres que tienen que escribir código que funcione con 2.6 / 2.5).

No creo que necesites exactamente la misma syntax de nombres que Python. Preferiría ir por una expresión regular simple como:

 \w+ 

para asegurarse de que sea algo alfanumérico, y luego agregue un prefijo para evitar la syntax de python. Así que la statement del usuario no técnico:

 LET return = 12 

Probablemente debería convertirse después de su análisis:

 userspace_return = 12 or userspace['return'] = 12