在Python中为表达式定义新的语义

Dmi*_* K. 6 python operator-overloading

我想基于Python定义约束规范语言.例如:

x = IntVar()
c = Constraint(x < 19)
c.solve()
Run Code Online (Sandbox Code Playgroud)

IntVar是一个描述可以采用任何整数值的变量的类,并且Constraint是一个表示约束的类.为了实现这一点,我可以<通过定义__lt__类的方法来重载操作符IntVar.

现在假设我想说明这一点10 < x < 19.我想写一些类似的东西:

c = Constraint(x > 10 and x < 19)
Run Code Online (Sandbox Code Playgroud)

不幸的是,我不能这样做,因为and不能在Python中重载.使用&而不是and因为它的优先级而不是一个选项,并且因为&在约束语言中逐位具有其正确的含义,例如,(x & 0x4) == 1.

您能建议什么解决方案?

作为一种解决方法,我使用带引号的表达式来约束:

c = Constraint("x < 19")
Run Code Online (Sandbox Code Playgroud)

但这需要实现我希望避免的约束语言解析,更重要的是,只有在实际完成解析时才能检查语法正确性.因此,用户可能花费几个小时来发现约束定义中存在语法错误.

我考虑的另一个选项是使用lambda表达式进行约束定义:

c = Constraint(lambda: x < 19)
Run Code Online (Sandbox Code Playgroud)

但我无法访问lambda-object的解析树.

Thi*_*ter 2

使用&, |and~实际上是一个不错的选择。您只需记录由于不同的运算符优先级而需要括号。

例如,SQLAlchemy 就是这样做的。对于不喜欢这种滥用按位运算符的人来说,它还提供了and_(*args)or_(*args)、 和not_(arg)函数,与对应的运算符执行相同的操作。但是,您被迫添加前缀符号 ( and_(foo, bar)),它的可读性不如中缀符号 ( foo & bar)。


这种lambda方法也是一个好主意(除了其本身带来的丑陋之外lambda)。不幸的是,如果没有源代码,AST 确实不可用 - 但是等等,你确实有源代码,只是没有附加到函数对象!

想象一下这段代码:

import ast
import inspect

def evaluate(constraint):
    print ast.dump(ast.parse(inspect.getsource(constraint)))

evaluate(lambda x: x < 5 and x > -5)
Run Code Online (Sandbox Code Playgroud)

这会给你这个 AST:

Module(
    body=[
        Expr(
            value=Call(
                func=Name(id='evaluate', ctx=Load()), args=[
                    Lambda(
                        args=arguments(
                            args=[
                                Name(id='x', ctx=Param())
                            ],
                            vararg=None,
                            kwarg=None,
                            defaults=[]
                        ),
                        body=BoolOp(
                            op=And(),
                            values=[
                                Compare(
                                    left=Name(id='x', ctx=Load()),
                                    ops=[Lt()],
                                    comparators=[Num(n=5)]
                                ),
                                Compare(
                                    left=Name(id='x', ctx=Load()),
                                    ops=[Gt()],
                                    comparators=[Num(n=-5)]
                                )
                            ]
                        )
                    )
                ],
                keywords=[],
                starargs=None,
                kwargs=None
            )
        )
    ]
)
Run Code Online (Sandbox Code Playgroud)

缺点是您可以获得整个源代码行 - 但您可以轻松地遍历 AST,直到到达 lambda 表达式(对评估函数的调用中的第一个表达式),然后您可以只处理相关部分。

为了避免必须自行计算它,您现在可以简单地重写 AST 以使用按位运算符,然后将新的 AST 编译为一个函数,该函数随后将使用可重载运算符。

让我们看一下 的 AST ((x < 5) & (x > -5))

body=BinOp(
    left=Compare(
        left=Name(id='x', ctx=Load()),
        ops=[Lt()],
        comparators=[Num(n=5)]
    ),
    op=BitAnd(),
    right=Compare(
        left=Name(id='x', ctx=Load()),
        ops=[Gt()],
        comparators=[Num(n=-5)]
    )
)
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,差异非常小。您只需要重写 AST 的 BoolOp 即可使用 BinOp!

的 ASTand_(x < 5, x > -5)看起来像这样:

body=Call(
    func=Name(id='and_', ctx=Load()),
    args=[
        Compare(
            left=Name(id='x', ctx=Load()),
            ops=[Lt()],
            comparators=[Num(n=5)]
        ),
        Compare(
            left=Name(id='x', ctx=Load()),
            ops=[Gt()],
            comparators=[Num(n=-5)]
        )
    ],
    keywords=[],
    starargs=None,
    kwargs=None
)
Run Code Online (Sandbox Code Playgroud)

重写也不太难。