在系数之间添加乘号 (*)

Add multiplication signs (*) between coefficients

我有一个程序,用户在其中输入一个函数,例如 sin(x)+1。我正在使用 ast 来尝试通过将组件列入白名单来确定字符串是否为 'safe',如 this answer 中所示。现在我想解析字符串以在没有它们的系数之间添加乘法 (*) 符号。

例如:

有什么有效的算法可以做到这一点吗?我更喜欢 pythonic 解决方案(即不是复杂的正则表达式,不是因为它们是 pythonic,而是因为我也不理解它们并且想要一个我能理解的解决方案。简单的正则表达式就可以了。)

我非常愿意在一个条件下使用 sympy(对于这类事情来说这看起来真的很容易):安全。显然 sympy 在幕后使用了 eval。我目前的(部分)解决方案非常安全。如果有人有办法使 sympy 使用不受信任的输入更安全,我也欢迎。

正则表达式是在 vanilla python 中完成工作的最快捷、最干净的方式,我什至会为您解释正则表达式,因为正则表达式是一个非常强大的工具,它很容易理解。

要实现您的目标,请使用以下语句:

import re
# <code goes here, set 'thefunction' variable to be the string you're parsing>
re.sub(r"((?:\d+)|(?:[a-zA-Z]\w*\(\w+\)))((?:[a-zA-Z]\w*)|\()", r"*", thefunction)

我知道它有点长和复杂,但是一个不同的、更简单的解决方案如果没有比这里的正则表达式中的内容更多的 hacky 内容,就不会立即显而易见。但是,这已经针对您的所有三个测试用例进行了测试,并且完全符合您的要求。

作为对这里发生的事情的简要解释: re.sub 的第一个参数是正则表达式,它匹配特定的模式。第二个是我们要用它替换的东西,第三个是要替换的实际字符串。每次我们的正则表达式看到匹配项时,它都会删除它并插入替换项,并使用一些特殊的幕后技巧.

对正则表达式的更深入分析如下:

  • ((?:\d+)|(?:[a-zA-Z]\w*\(\w+\)))((?:[a-zA-Z]\w*)|\() :匹配数字或函数调用,后跟变量或括号。
    • ((?:\d+)|(?:[a-zA-Z]\w*\(\w+\)))第 1 组。注意:括号分隔了一个组,它是一种子正则表达式。捕获组已编入索引以供将来参考;组也可以用修饰符重复(稍后描述)。该组匹配一个数字或一个函数调用。
      • (?:\d+) :非捕获组。任何在左括号后紧跟 ?: 的组都不会为其自身分配索引,但仍充当模式的 "section"。前任。 A(?:bc)+ 将匹配 "Abcbcbcbc..." 等等,但是您不能访问带有索引的 "bcbcbcbc" 匹配。但是,如果没有这个组,写 "Abc+" 将匹配 "Abcccccccc..."
        • \d :匹配任何数字一次。 \d 的正则表达式将单独匹配 "1""2""123""3"
        • + :匹配前一个元素一次或多次次。在这种情况下,前一个元素是 \d,任意数字。在前面的示例中,“123”上的 \d+ 将成功匹配“123”作为单个元素。这对我们的正则表达式至关重要,以确保正确注册多位数。
      • | :管道字符,在正则表达式中,它实际上表示 or"a|b" 将匹配 "a""b"。在这种情况下,它将 "a number" 和 "a function call" 分开;匹配数字或函数调用。
      • (?:[a-zA-Z]\w*\(\w+\)) :匹配函数调用。也是一个非捕获组,如 (?:\d+)
        • [a-zA-Z] :匹配函数调用的第一个字母。这里没有修饰符,因为我们只需要确保 first 字符是一个字母即可; A123 在技术上是一个有效的函数名称。
        • \w :匹配任何字母数字字符或下划线。确保第一个字母后,后面的字符可以是字母、数字或下划线,仍然可以作为函数名。
        • * :匹配前一个元素0次或更多次。虽然最初看起来没有必要,但星号实际上使元素成为 可选 。在本例中,我们修改的元素是 \w,但一个函数在技术上不需要超过一个字符; A() 是一个有效的函数名。 A 将与 [a-zA-Z] 匹配,使得 \w 变得不必要。另一方面,第一个字母后可以有任意数量的字符,这就是我们需要此修饰符的原因。
        • \( :理解这一点很重要:这不是另一个组。这里的反斜杠很像普通字符串中的转义字符。在正则表达式中,任何时候您在特殊字符前加上反斜杠,例如圆括号、+*,它都像普通字符一样使用它。 \( 匹配 一个左括号 ,用于函数的实际函数调用部分。
        • \w+ :匹配数字、字母或下划线一次或多次。这确保函数实际上有一个参数进入它。
        • \) :类似于 \(,但匹配 括号
    • ((?:[a-zA-Z]\w*)|\()第 2 组。匹配变量或左括号。
      • (?:[a-zA-Z]\w*) :匹配一个变量。这与我们的函数名称匹配器完全相同。但是,请注意这是在非捕获组中:这很重要,因为 OR 检查的方式。紧随其后的 OR 将这个组视为一个整体。如果不分组,"last object matched" 将是 \w*,这不足以满足我们的需求。它会说:"match one letter followed by more letters OR one letter followed by a parenthesis"。将此元素放在非捕获组中允许我们控制 OR 注册的内容。
      • | : 或字符。匹配 (?:[a-zA-Z]\w*)\(.
      • \( :匹配左括号。一旦我们检查了是否有左括号,就我们的正则表达式而言,我们不需要检查任何超出它的内容。

现在,还记得我们的两个小组,第一组和第二组吗?这些用于替换字符串 "*"。替换字符串不是真正的正则表达式,但它仍然具有某些特殊字符。在这种情况下,\<number> 将插入该号码的组。所以我们的替换字符串是这样的:"Put group 1 in (which is either our function call or our number), then put in an asterisk (*), then put in our second group (either a variable or a parenthesis)"

我想大概总结一下吧!