Añadir signos de multiplicación (*) entre coeficientes

Tengo un progtwig en el que un usuario ingresa una función, como sin(x)+1 . Estoy usando ast para tratar de determinar si la cadena es ‘segura’ al incluir en la lista blanca los componentes como se muestra en esta respuesta . Ahora me gustaría analizar la cadena para agregar signos de multiplicación ( * ) entre los coeficientes sin ellos.

Por ejemplo:

  • 3x -> 3*x
  • 4(x+5) -> 4*(x+5)
  • sin(3x)(4) -> sin(3x)*(4) (el sin ya está en los globales, de lo contrario sería s*i*n*(3x)*(4)

¿Hay algoritmos eficientes para lograr esto? Prefiero una solución pythonica (es decir, no expresiones regulares complejas, no porque sean pirónicas, sino simplemente porque no las entiendo bien y quiero una solución que pueda comprender. Las expresiones regulares simples están bien).

Estoy muy abierto a usar sympy (que parece muy fácil para este tipo de cosas) bajo una condición: seguridad. Al parecer, sympy utiliza eval bajo el capó. Tengo bastante buena seguridad con mi solución actual (parcial). Si alguien tiene una manera de hacer que sympy más seguro con información no confiable, también lo agradecería.

Una expresión regular es fácilmente la forma más rápida y limpia de hacer el trabajo en Python Vanilla, e incluso le explicaré la expresión regular para usted, porque las expresiones regulares son una herramienta tan poderosa que es agradable de entender.

Para lograr su objective, use la siguiente statement:

 import re #  re.sub(r"((?:\d+)|(?:[a-zA-Z]\w*\(\w+\)))((?:[a-zA-Z]\w*)|\()", r"\1*\2", thefunction) 

Sé que es un poco largo y complicado, pero una solución diferente, más simple, no se hace evidente de inmediato, sin que haya más cosas intrépidas que las que se incluyen aquí en la expresión regular. Pero, esto ha sido probado en sus tres casos de prueba y funciona exactamente como lo desea.

Como una breve explicación de lo que está pasando aquí: el primer parámetro para re.sub es la expresión regular, que coincide con un determinado patrón. El segundo es aquello con lo que lo estamos reemplazando, y el tercero es la cadena real para reemplazar elementos. Cada vez que nuestro regex ve una coincidencia, lo elimina y conecta la sustitución, con algunos trucos especiales entre bastidores. .

A continuación un análisis más profundo de la expresión regular:

  • ((?:\d+)|(?:[a-zA-Z]\w*\(\w+\)))((?:[a-zA-Z]\w*)|\() : Matches un número o una llamada de función, seguido de una variable o paréntesis.
    • ((?:\d+)|(?:[a-zA-Z]\w*\(\w+\))) : Grupo 1 . Nota: Los paréntesis delimitan un grupo, que es una especie de subexpresión. Los grupos de captura se indexan para referencia futura; Los grupos también se pueden repetir con modificadores (descritos más adelante). Este grupo coincide con un número o una llamada de función.
      • (?:\d+) : Grupo que no captura. Cualquier grupo con ?: Inmediatamente después del paréntesis de apertura no se asignará un índice a sí mismo, pero seguirá actuando como una "sección" del patrón. Ex. A(?:bc)+ coincidirá con "Abcbcbcbc ..." y así sucesivamente, pero no puede acceder a la coincidencia "bcbcbcbc" con un índice. Sin embargo, sin este grupo, escribir "Abc +" coincidiría con "Abcccccccc ..."
        • \d : hace coincidir cualquier dígito numérico una vez. Una expresión regular de \d todo lo suyo coincidirá, por separado, con "1" , "2" y "3" de "123" .
        • + : Hace coincidir el elemento anterior una o más veces. En este caso, el elemento anterior es \d , cualquier número. En el ejemplo anterior, \d+ en "123" coincidirá con éxito con "123" como un solo elemento. Esto es vital para nuestra expresión regular, para asegurarnos de que los números de varios dígitos estén registrados correctamente.
      • | : Carácter de canalización, y en una expresión regular, efectivamente dice or : "a|b" coincidirá con "a" OR "b" . En este caso, separa "un número" y "una llamada de función"; hacer coincidir un número O una llamada de función.
      • (?:[a-zA-Z]\w*\(\w+\)) : coincide con una llamada de función. También un grupo que no captura, como (?:\d+) .
        • [a-zA-Z] : coincide con la primera letra de la llamada a la función. No hay ningún modificador en esto porque solo necesitamos asegurarnos de que el primer carácter sea una letra; A123 es técnicamente un nombre de función válido.
        • \w : coincide con cualquier carácter alfanumérico o un guión bajo. Después de asegurar la primera letra, los siguientes caracteres pueden ser letras, números o guiones bajos y seguir siendo válidos como nombre de función.
        • * : Coincide con el elemento anterior 0 o más veces. Aunque inicialmente parece innecesario, el personaje estrella efectivamente hace que un elemento sea opcional . En este caso, nuestro elemento modificado es \w , pero una función no necesita técnicamente más de un carácter; A() es un nombre de función válido. A sería igualado por [a-zA-Z] , haciendo \w innecesario. En el otro extremo del espectro, podría haber cualquier número de caracteres después de la primera letra, por lo que necesitamos este modificador.
        • \( : Esto es importante de entender: este no es otro grupo . La barra diagonal inversa aquí actúa como lo haría un personaje de escape en una cadena normal. En una expresión regular, cada vez que introduzca un carácter especial, como paréntesis, + , o * con una barra invertida, lo utiliza como un carácter normal. \( coincide con un paréntesis de apertura , para la función actual, que forma parte de la función.
        • \w+ : hace coincidir un número, una letra o un guión bajo una o más veces. Esto asegura que la función realmente tenga un parámetro en ella.
        • \) : Me gusta \( , pero coincide con un paréntesis de cierre
    • ((?:[a-zA-Z]\w*)|\() : Grupo 2. Coincide con una variable, o un paréntesis de apertura.
      • (?:[a-zA-Z]\w*) : coincide con una variable. Este es exactamente el mismo que nuestro igualador de nombre de función. Sin embargo, tenga en cuenta que esto está en un grupo que no captura: esto es importante, debido a la forma en que el OR verifica. El OR inmediatamente después de esto mira a este grupo como un todo. Si esto no estuviera agrupado, el "último objeto coincidente" sería \w* , lo que no sería suficiente para lo que queremos. Diría: "coincide una letra seguida de más letras O una letra seguida de un paréntesis". Poner este elemento en un grupo sin captura nos permite controlar lo que registra el OR.
      • | : O personaje. Coincidencias (?:[a-zA-Z]\w*) o \( .
      • \( : Coincide con un paréntesis de apertura. Una vez que hayamos comprobado si existe un paréntesis de apertura, no es necesario que verifiquemos nada más allá de esto para los fines de nuestra expresión regular.

Ahora, ¿recuerdas nuestros dos grupos, el grupo uno y el grupo dos? Estos se utilizan en la cadena de sustitución, "\1*\2" . La cadena de sustitución no es una expresión regular verdadera, pero aún tiene ciertos caracteres especiales. En este caso, \ insertará el grupo de ese número. Así que nuestra cadena de sustitución dice: "Ponga el grupo 1 en (que es nuestra llamada de función o nuestro número), luego coloque un asterisco (*), luego ingrese nuestro segundo grupo (una variable o un paréntesis)"

¡Creo que eso lo resume todo!