通过符号访问运算符函数

7 投票
4 回答
2676 浏览
提问于 2025-04-17 14:45

我需要一个函数,这个函数可以接收一个字符串形式的Python运算符符号或关键字,还有它的操作数,然后计算结果并返回。就像这样:

>>> string_op('<=', 3, 3)
True
>>> string_op('|', 3, 5)
7
>>> string_op('and', 3, 5)
True
>>> string_op('+', 5, 7)
12
>>> string_op('-', -4)
4

这个字符串不能被认为是安全的。我只想把二元运算符映射到函数上,但如果能把所有运算符都搞定,那就更好了。

我现在的实现是手动把符号映射到运算符模块中的函数:

import operator

def string_op(op, *args, **kwargs):
    """http://docs.python.org/2/library/operator.html"""
    symbol_name_map = {
        '<': 'lt',
        '<=': 'le',
        '==': 'eq',
        '!=': 'ne',
        '>=': 'ge',
        '>': 'gt',
        'not': 'not_',
        'is': 'is_',
        'is not': 'is_not',
        '+': 'add', # conflict with concat
        '&': 'and_', # (bitwise)
        '/': 'div',
        '//': 'floordiv',
        '~': 'invert',
        '%': 'mod',
        '*': 'mul',
        '|': 'or_', # (bitwise)
        'pos': 'pos_',
        '**': 'pow',
        '-': 'sub', # conflicts with neg
        '^': 'xor',
        'in': 'contains',
        '+=': 'iadd', # conflict with iconcat
        '&=': 'iand',
        '/=': 'idiv',
        '//=': 'ifloordiv',
        '<<=': 'ilshift',
        '%=': 'imod',
        '*=': 'imul',
        '|=': 'ior',
        '**=': 'ipow',
        '>>=': 'irshift',
        '-=': 'isub',
        '^=': 'ixor',
    }
    if op in symbol_name_map:
        return getattr(operator, symbol_name_map[op])(*args, **kwargs)
    else:
        return getattr(operator, op)(*args, **kwargs)

这个方案在一些重载的运算符上会失败,比如 add/concatsub/neg。可以添加一些检查来识别这些情况,检测类型或参数数量来选择正确的函数名,但这样感觉有点麻烦。如果这里没有更好的主意,我就只能这样做了。

让我困惑的是,Python 其实已经能做到这一点。它已经知道如何把符号映射到运算符函数上,但到目前为止,我发现这个功能并没有暴露给程序员。看起来Python的其他所有功能,包括序列化协议,都可以让程序员使用。那么这个功能在哪里呢?或者说,为什么没有呢?

4 个回答

2

如果你打算使用这样的映射,为什么不直接把它映射到函数上,而不是通过名字来间接引用呢?比如:

symbol_func_map = {
    '<': (lambda x, y: x < y),
    '<=': (lambda x, y: x <= y),
    '==': (lambda x, y: x == y),
    #...
}

虽然这样做并不会比你现在的实现更简洁,但在大多数情况下,它应该能得到正确的行为。剩下的问题是当一个一元操作符和一个二元操作符发生冲突时,这可以通过在字典的键中添加参数数量来解决:

symbol_func_map = {
    ('<', 2): (lambda x, y: x < y),
    ('<=', 2): (lambda x, y: x <= y),
    ('==', 2): (lambda x, y: x == y),
    ('-', 2): (lambda x, y: x - y),
    ('-', 1): (lambda x: -x),
    #...
}
3

你可以用一个简单的正则表达式。我们可以这样做:

import re, operator

def get_symbol(op):
    sym = re.sub(r'.*\w\s?(\S+)\s?\w.*','\\1',getattr(operator,op).__doc__)
    if re.match('^\\W+$',sym):return sym

举几个例子:

 get_symbol('matmul')
'@'
get_symbol('add')
 '+'
get_symbol('eq')
'=='
get_symbol('le')
'<='
get_symbol('mod')
'%'
get_symbol('inv')
'~'
get_symbol('ne')
'!='

这只是其中的一些。你还可以这样做:

{get_symbol(i):i for i in operator.__all__} 

这样你就能得到一个包含符号的字典。你会发现像 abs 这样的东西会给出错误,因为没有实现符号版本。

6

Python并不是把符号直接对应到operator函数上,而是通过调用一些特殊的方法来理解这些符号。

比如,当你写2 * 3时,Python并不是调用mul(2, 3)这个函数,而是运行一些C语言的代码,来判断应该使用two.__mul__three.__rmul__,还是它们的C语言对应版本(nb_multiplysq_repeat这两个槽位都可以看作是__mul____rmul__的替代)。你也可以通过C扩展模块来调用这段代码,像这样:PyNumber_Multiply(two, three)。如果你查看operator.mul的源代码,会发现它是一个完全独立的函数,但它调用的也是PyNumber_Multiply

所以,Python并没有把*这个符号直接映射到operator.mul上。

如果你想通过编程的方式来实现这个映射,我能想到的最好方法就是解析operator函数的文档字符串(或者,也许是operator.c的源代码)。例如:

runary = re.compile(r'Same as (.+)a')
rbinary = re.compile(r'Same as a (.+) b')
unary_ops, binary_ops = {}, {}
funcnames = dir(operator)
for funcname in funcnames:
    if (not funcname.startswith('_') and
        not (funcname.startswith('r') and funcname[1:] in funcnames) and
        not (funcname.startswith('i') and funcname[1:] in funcnames)):
        func = getattr(operator, funcname)
        doc = func.__doc__
        m = runary.search(doc)
        if m:
            unary_ops[m.group(1)] = func
        m = rbinary.search(doc)
        if m:
            binary_ops[m.group(1)] = func

我觉得这个方法没有遗漏什么,但确实会有一些误判,比如"a + b, for a "被当作映射到operator.concat的操作符,callable(被当作映射到operator.isCallable的操作符。(具体的结果取决于你的Python版本。)你可以根据需要调整正则表达式,黑名单一些方法等等。

不过,如果你真的想写一个解析器,可能写一个针对你实际语言的解析器会比写一个解析文档字符串的解析器要好。

如果你想解析的语言是Python的一个子集,Python确实提供了一些内部功能来帮助你。可以查看ast模块作为起点。你可能会更喜欢使用像pyparsing这样的工具,但至少应该试试ast。例如:

sentinel = object()
def string_op(op, arg1, arg2=sentinel):
    s = '{} {}'.format(op, arg1) if arg2 is sentinel else '{} {} {}'.format(op, arg1, arg2)
    a = ast.parse(s).body

打印出a(或者更好的是ast.dump(a)),玩玩它等等。不过,你仍然需要把_ast.Add映射到operator.add。但是如果你想映射到一个实际的Pythoncode对象……那么,这段代码也是可以找到的。

撰写回答