Se puede añadir nuevas declaraciones (como print
, raise
, with
) a la sintaxis de Python?
Decir, para permitir ..
mystatement "Something"
O,
new_if True:
print "example"
No tanto si debería , sino más bien si es posible (salvo modificar el código de los intérpretes de Python)
Respuestas:
Esto puede resultarle útil: aspectos internos de Python: agregar una nueva declaración a Python , citada aquí:
Este artículo es un intento de comprender mejor cómo funciona el front-end de Python. Solo leer la documentación y el código fuente puede ser un poco aburrido, así que estoy adoptando un enfoque práctico aquí: voy a agregar una
until
declaración a Python.Toda la codificación de este artículo se realizó contra la rama Py3k de última generación en el espejo del repositorio Python Mercurial .
La
until
declaraciónAlgunos idiomas, como Ruby, tienen una
until
declaración, que es el complemento dewhile
(until num == 0
es equivalente awhile num != 0
). En Ruby, puedo escribir:Y se imprimirá:
Entonces, quiero agregar una capacidad similar a Python. Es decir, poder escribir:
Una digresión de defensa del lenguaje
Este artículo no intenta sugerir la adición de una
until
declaración a Python. Aunque creo que tal afirmación aclararía un poco el código, y este artículo muestra lo fácil que es agregarlo, respeto completamente la filosofía de minimalismo de Python. Todo lo que estoy tratando de hacer aquí, realmente, es obtener una idea del funcionamiento interno de Python.Modificando la gramática
Python usa un generador de analizador personalizado llamado
pgen
. Este es un analizador LL (1) que convierte el código fuente de Python en un árbol de análisis. La entrada al generador de analizadores es el archivoGrammar/Grammar
[1] . Este es un archivo de texto simple que especifica la gramática de Python.[1] : De aquí en adelante, las referencias a los archivos en la fuente de Python se dan relativamente a la raíz del árbol de fuentes, que es el directorio donde ejecuta configure y crea para construir Python.
Deben realizarse dos modificaciones al archivo de gramática. El primero es agregar una definición para la
until
declaración. Encontré dóndewhile
se definió la declaración (while_stmt
), y agregué auntil_stmt
continuación [2] :[2] : Esto demuestra una técnica común que uso al modificar el código fuente con el que no estoy familiarizado: trabajar por similitud . Este principio no resolverá todos sus problemas, pero definitivamente puede facilitar el proceso. Dado que todo lo que debe hacerse
while
también debe hacerseuntil
, sirve como una guía bastante buena.Tenga en cuenta que he decidido excluir la
else
cláusula de mi definición deuntil
, solo para que sea un poco diferente (y porque, francamente, no me gusta laelse
cláusula de bucles y no creo que encaje bien con el Zen de Python).El segundo cambio es modificar la regla para
compound_stmt
incluiruntil_stmt
, como puede ver en el fragmento de arriba. Es justo despuéswhile_stmt
, de nuevo.Cuando se ejecuta
make
después de modificarGrammar/Grammar
, observe que elpgen
programa se ejecuta para volver a generarInclude/graminit.h
yPython/graminit.c
, a continuación, varios archivos se vuelven a compilar.Modificando el código de generación AST
Después de que el analizador de Python haya creado un árbol de análisis, este árbol se convierte en un AST, ya que los AST son mucho más simples para trabajar en las etapas posteriores del proceso de compilación.
Entonces, vamos a visitar
Parser/Python.asdl
qué define la estructura de los AST de Python y agregaremos un nodo AST para nuestra nuevauntil
declaración, nuevamente justo debajo dewhile
:Si ahora ejecuta
make
, tenga en cuenta que antes de compilar un montón de archivos,Parser/asdl_c.py
se ejecuta para generar código C a partir del archivo de definición AST. Este (comoGrammar/Grammar
) es otro ejemplo del código fuente de Python usando un mini-lenguaje (en otras palabras, un DSL) para simplificar la programación. También tenga en cuenta que, dado queParser/asdl_c.py
es un script de Python, este es un tipo de arranque : para construir Python desde cero, Python ya tiene que estar disponible.Si bien
Parser/asdl_c.py
generamos el código para administrar nuestro nodo AST recién definido (en los archivosInclude/Python-ast.h
yPython/Python-ast.c
), todavía tenemos que escribir el código que convierte a mano un nodo de árbol de análisis relevante. Esto se hace en el archivoPython/ast.c
. Allí, una función llamadaast_for_stmt
convierte los nodos del árbol de análisis para las declaraciones en nodos AST. Nuevamente, guiados por nuestro viejo amigowhile
, saltamos directamente a lo grandeswitch
para manejar declaraciones compuestas y agregamos una cláusula parauntil_stmt
:Ahora deberíamos implementar
ast_for_until_stmt
. Aquí está:Una vez más, esto fue codificado mientras miraba de cerca el equivalente
ast_for_while_stmt
, con la diferencia de queuntil
he decidido no apoyar laelse
cláusula. Como se esperaba, el AST se crea de forma recursiva, utilizando otras funciones de creación de AST, comoast_for_expr
la expresión de condición yast_for_suite
el cuerpo de launtil
declaración. Finalmente,Until
se devuelve un nuevo nodo llamado .Tenga en cuenta que accedemos al nodo parse-tree
n
usando algunas macros comoNCH
yCHILD
. Vale la pena entenderlos: su código está enInclude/node.h
.Digresión: composición AST
Elegí crear un nuevo tipo de AST para la
until
declaración, pero en realidad esto no es necesario. Podría haber ahorrado algo de trabajo e implementado la nueva funcionalidad usando la composición de los nodos AST existentes, ya que:Es funcionalmente equivalente a:
En lugar de crear el
Until
nodoast_for_until_stmt
, podría haber creado unNot
nodo con unWhile
nodo cuando era niño. Como el compilador AST ya sabe cómo manejar estos nodos, se pueden omitir los siguientes pasos del proceso.Compilación de AST en bytecode
El siguiente paso es compilar el AST en Python bytecode. La compilación tiene un resultado intermedio que es un CFG (Control Flow Graph), pero como el mismo código lo maneja, ignoraré este detalle por ahora y lo dejaré para otro artículo.
El código que veremos a continuación es
Python/compile.c
. Siguiendo el ejemplo dewhile
, encontramos la funcióncompiler_visit_stmt
, que es responsable de compilar las declaraciones en bytecode. Agregamos una cláusula paraUntil
:Si se pregunta qué
Until_kind
es, es una constante (en realidad un valor de la_stmt_kind
enumeración) generada automáticamente desde el archivo de definición ASTInclude/Python-ast.h
. De todos modos, llamamoscompiler_until
que, por supuesto, todavía no existe. Llegaré a eso un momento.Si tienes curiosidad como yo, notarás que
compiler_visit_stmt
es peculiar. Ninguna cantidad degrep
-ping del árbol fuente revela dónde se llama. Cuando este es el caso, solo queda una opción: C macro-fu. De hecho, una breve investigación nos lleva a laVISIT
macro definida enPython/compile.c
:Se utiliza para invocar
compiler_visit_stmt
encompiler_body
. Volviendo a nuestro negocio, sin embargo ...Según lo prometido, aquí está
compiler_until
:Tengo una confesión que hacer: este código no fue escrito en base a un profundo conocimiento del código de bytes Python. Al igual que el resto del artículo, se realizó imitando la
compiler_while
función de parentesco . Sin embargo, al leerlo detenidamente, teniendo en cuenta que la máquina virtual de Python está basada en la pila, y mirando la documentación deldis
módulo, que tiene una lista de códigos de bytes de Python con descripciones, es posible comprender lo que está sucediendo.Eso es todo, hemos terminado ... ¿no?
Después de hacer todos los cambios y ejecutar
make
, podemos ejecutar el Python recién compilado y probar nuestra nuevauntil
declaración:¡Voila, funciona! Veamos el bytecode creado para la nueva declaración usando el
dis
módulo de la siguiente manera:Aquí está el resultado:
La operación más interesante es la número 12: si la condición es verdadera, saltamos a después del ciclo. Esta es la semántica correcta para
until
. Si no se ejecuta el salto, el cuerpo del bucle sigue corriendo hasta que vuelve a la condición en la operación 35.Sintiéndome bien con mi cambio, intenté ejecutar la función (ejecutar
myfoo(3)
) en lugar de mostrar su código de bytes. El resultado fue menos que alentador:Whoa ... esto no puede ser bueno. Entonces, ¿qué salió mal?
El caso de la tabla de símbolos que falta
Uno de los pasos que realiza el compilador de Python al compilar el AST es crear una tabla de símbolos para el código que compila. La llamada a
PySymtable_Build
inPyAST_Compile
llama al módulo de tabla de símbolos (Python/symtable.c
), que recorre el AST de manera similar a las funciones de generación de código. Tener una tabla de símbolos para cada ámbito ayuda al compilador a encontrar información clave, como qué variables son globales y cuáles son locales para un ámbito.Para solucionar el problema, tenemos que modificar la
symtable_visit_stmt
funciónPython/symtable.c
, agregando código para manejar lasuntil
declaraciones, después del código similar para laswhile
declaraciones [3] :[3] : Por cierto, sin este código hay una advertencia del compilador
Python/symtable.c
. El compilador nota que elUntil_kind
valor de enumeración no se maneja en la declaración de cambiosymtable_visit_stmt
y se queja. ¡Siempre es importante verificar las advertencias del compilador!Y ahora realmente hemos terminado. Compilar la fuente después de este cambio hace que la ejecución del
myfoo(3)
trabajo sea la esperada.Conclusión
En este artículo, he demostrado cómo agregar una nueva declaración a Python. Aunque requirió un poco de retoques en el código del compilador de Python, el cambio no fue difícil de implementar, porque utilicé una declaración similar y existente como guía.
El compilador de Python es una parte sofisticada de software, y no pretendo ser un experto en él. Sin embargo, estoy realmente interesado en los aspectos internos de Python, y particularmente en su front-end. Por lo tanto, este ejercicio me pareció un compañero muy útil para el estudio teórico de los principios y el código fuente del compilador. Servirá como base para futuros artículos que profundizarán en el compilador.
Referencias
Utilicé algunas referencias excelentes para la construcción de este artículo. Aquí están, sin ningún orden en particular:
fuente original
fuente
until
esisa
/isan
as enif something isa dict:
oif something isan int:
Una forma de hacer cosas como esta es preprocesar la fuente y modificarla, traduciendo su declaración agregada a python. Hay varios problemas que este enfoque traerá, y no lo recomendaría para uso general, pero para la experimentación con el lenguaje o la metaprogramación de propósito específico, ocasionalmente puede ser útil.
Por ejemplo, supongamos que queremos introducir una declaración "myprint" que, en lugar de imprimir en la pantalla, inicie sesión en un archivo específico. es decir:
sería equivalente a
Hay varias opciones sobre cómo hacer el reemplazo, desde la sustitución de expresiones regulares hasta la generación de un AST, hasta escribir su propio analizador dependiendo de qué tan cerca coincida su sintaxis con la pitón existente. Un buen enfoque intermedio es usar el módulo tokenizer. Esto debería permitirle agregar nuevas palabras clave, estructuras de control, etc. al interpretar la fuente de manera similar al intérprete de Python, evitando así la rotura que causarían las soluciones crudas de expresiones regulares. Para el "myprint" anterior, puede escribir el siguiente código de transformación:
(Esto hace que myprint sea efectivamente una palabra clave, por lo que su uso como variable en otros lugares probablemente causará problemas)
El problema es cómo usarlo para que su código sea utilizable desde Python. Una forma sería escribir su propia función de importación y usarla para cargar el código escrito en su idioma personalizado. es decir:
Sin embargo, esto requiere que maneje su código personalizado de manera diferente a los módulos de Python normales. es decir, "
some_mod = myimport("some_mod.py")
" en lugar de "import some_mod
"Otra solución bastante clara (aunque hacky) es crear una codificación personalizada (ver PEP 263 ) como lo demuestra esta receta. Puede implementar esto como:
Ahora, después de ejecutar este código (por ejemplo, puede colocarlo en su .pythonrc o site.py), cualquier código que comience con el comentario "# coding: mylang" se traducirá automáticamente a través del paso de preprocesamiento anterior. p.ej.
Advertencias:
El enfoque del preprocesador tiene problemas, ya que probablemente estará familiarizado si ha trabajado con el preprocesador C. El principal es la depuración. Todo lo que ve Python es el archivo preprocesado, lo que significa que el texto impreso en el seguimiento de la pila, etc. Si ha realizado una traducción significativa, esto puede ser muy diferente de su texto fuente. El ejemplo anterior no cambia los números de línea, etc., por lo que no será muy diferente, pero cuanto más lo cambie, más difícil será descubrirlo.
fuente
myimport
en un módulo que contiene simplementeprint 1
, ya que es la única línea de los rendimientos de código=1 ... SyntaxError: invalid syntax
b=myimport("b.py")
" y b.py que contienen solo "print 1
". ¿Hay algo más en el error? etc)import
usa el builtin incorporado__import__
, por lo que si sobrescribe eso ( antes de importar el módulo que requiere la importación modificada), no necesita un separadomyimport
Sí, hasta cierto punto es posible. Hay un módulo que se usa
sys.settrace()
para implementargoto
ycomefrom
"palabras clave":fuente
A falta de cambiar y volver a compilar el código fuente (que es posible con el código abierto), cambiar el idioma base no es realmente posible.
Incluso si vuelve a compilar la fuente, no sería Python, solo su versión modificada pirateada, que debe tener mucho cuidado de no introducir errores.
Sin embargo, no estoy seguro de por qué querrías hacerlo. Las características orientadas a objetos de Python hacen que sea bastante simple lograr resultados similares con el lenguaje tal como está.
fuente
Respuesta general: debe preprocesar sus archivos fuente.
Respuesta más específica: instalar EasyExtend y siga los siguientes pasos
i) Crear un nuevo langlet (idioma de extensión)
Sin una especificación adicional, se creará un conjunto de archivos en EasyExtend / langlets / mystmts /.
ii) Abra mystmts / parsedef / Grammar.ext y agregue las siguientes líneas
Esto es suficiente para definir la sintaxis de su nueva declaración. Small_stmt no terminal es parte de la gramática de Python y es el lugar donde se engancha la nueva declaración. El analizador ahora reconocerá la nueva declaración, es decir, se analizará un archivo fuente que la contenga. Sin embargo, el compilador lo rechazará porque todavía tiene que transformarse en Python válido.
iii) Ahora hay que agregar la semántica de la declaración. Para esto tiene que editar msytmts / langlet.py y agregar un visitante de nodo my_stmt.
iv) cd a langlets / mystmts y escriba
Ahora se iniciará una sesión y se puede usar la nueva declaración definida:
Unos cuantos pasos para llegar a una declaración trivial, ¿verdad? Todavía no hay una API que permita definir cosas simples sin tener que preocuparse por las gramáticas. Pero EE es muy confiable módulo algunos errores. Por lo tanto, es solo cuestión de tiempo que surja una API que permita a los programadores definir cosas convenientes como operadores de infijo o declaraciones pequeñas utilizando solo la programación OO conveniente. Para cosas más complejas como incrustar lenguajes enteros en Python mediante la construcción de un langlet, no hay forma de evitar un enfoque gramatical completo.
fuente
Aquí hay una manera muy simple pero horrible de agregar nuevas declaraciones, solo en modo interpretativo . Lo estoy usando para pequeños comandos de 1 letra para editar anotaciones genéticas usando solo sys.displayhook, pero para poder responder a esta pregunta también agregué sys.excepthook para los errores de sintaxis. Este último es realmente feo, recuperando el código sin procesar del búfer de línea de lectura. El beneficio es que es trivialmente fácil agregar nuevas declaraciones de esta manera.
fuente
He encontrado una guía sobre cómo agregar nuevas declaraciones:
https://troeger.eu/files/teaching/pythonvm08lab.pdf
Básicamente, para agregar nuevas declaraciones, debe editar
Python/ast.c
(entre otras cosas) y volver a compilar el binario de Python.Si bien es posible, no lo hagas. Puede lograr casi todo a través de funciones y clases (que no requerirán que las personas recompilen Python solo para ejecutar su script ...)
fuente
Es posible hacer esto usando EasyExtend :
fuente
No es exactamente agregar nuevas declaraciones a la sintaxis del lenguaje, pero las macros son una herramienta poderosa: https://github.com/lihaoyi/macropy
fuente
No sin modificar el intérprete. Sé que muchos idiomas en los últimos años han sido descritos como "extensibles", pero no en la forma en que lo estás describiendo. Extiende Python agregando funciones y clases.
fuente
Hay un lenguaje basado en Python llamado Logix con el que PUEDES hacer tales cosas. No ha estado en desarrollo durante un tiempo, pero las características que solicitó funcionan con la última versión.
fuente
Algunas cosas se pueden hacer con decoradores. Supongamos, por ejemplo, que Python no tenía ninguna
with
declaración. Entonces podríamos implementar un comportamiento similar como este:Sin embargo, es una solución bastante sucia como se hace aquí. Especialmente el comportamiento donde el decorador llama a la función y se pone
_
aNone
es inesperado. Para aclarar: este decorador es equivalente a escribiry normalmente se espera que los decoradores modifiquen, no ejecuten, funciones.
Utilicé dicho método antes en un script donde tuve que configurar temporalmente el directorio de trabajo para varias funciones.
fuente
Hace diez años no podías, y dudo que eso haya cambiado. Sin embargo, no fue tan difícil modificar la sintaxis en ese entonces si estaba preparado para recompilar Python, y dudo que eso haya cambiado, tampoco.
fuente