6

I need a function which takes one of python's operator symbols or keywords as a string, along with its operands, evaluates it, and returns the result. Like this:

>>> 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

The string cannot be assumed to be safe. I will be satisfied with just mapping the binary operators, but I'd be extra happy if I could get all of them.

My current implementation manually maps the symbols to the functions in the operator module:

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)

This solution fails on the overloaded operators -- add/concat and sub/neg. Checks could be added to detect those cases and detect types or count arguments to pick the right function name, but that feels a bit ugly. It's what I'll go with if I don't get a better idea here.

The thing that is bugging me is that python already does this. It already knows how to map symbols to operator functions, but so far as I can tell, that functionality is not exposed to the programmer. Seems like everything else in python, right down to the pickling protocol, is exposed to programmers. So where is this? or why isn't it?

4

4 回答 4

6

Python 不会符号映射到operator函数。dunder它通过调用特殊方法来解释符号。

例如,当您编写时2 * 3,它不会调用mul(2, 3); 它调用一些 C 代码来确定是使用two.__mul__,three.__rmul__还是 C 类型的等效项(插槽nb_multiplysq_repeat都等效于__mul__and __rmul__)。您可以从 C 扩展模块调用相同的代码作为PyNumber_Multiply(two, three). 如果您查看 的源代码operator.mul,它是一个完全独立的函数,它调用相同的PyNumber_Multiply.

因此,Python 没有从*tooperator.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 版本。)随意调整正则表达式,将此类方法列入黑名单等以品尝。

然而,如果你真的想写一个解析器,你最好为你的实际语言编写一个解析器,而不是为文档字符串编写一个解析器来生成你的语言解析器……</p>

如果您尝试解析的语言是 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)),玩弄它,等等。但是,你仍然需要映射 from_ast.Addoperator.add。但是,如果您想映射到实际的 Pythoncode对象……那么,也可以使用相应的代码。

于 2013-02-04T21:30:53.720 回答
2

如果要使用这样的映射,为什么不直接映射到函数而不是按名称进行间接层呢?例如:

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

虽然这不会比您当前的实现更简洁,但在大多数情况下它应该得到正确的行为。剩下的问题是一元和二元运算符冲突的地方,可以通过向字典键添加 arity 来解决这些问题:

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),
    #...
}
于 2013-02-04T22:34:22.540 回答
1

您可以使用粗略的正则表达式。我们可以做的:

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给出的东西不正确,因为没有实现符号版本

于 2019-09-12T18:57:41.363 回答
0

您可以使用 eval 为运算符生成 lambda 函数,而不是使用operator模块。Eval 通常是不好的做法,但我认为为此目的它很好,因为它并不是真正疯狂的。

def make_binary_op(symbol):
    return eval('lambda x, y: x {0} y'.format(symbol))

operators = {}
for operator in '+ - * / ^ % (etc...)'.split(' '):
    operators[operator] = make_binary_op(operator)

operators['*'](3, 5) # == 15
于 2014-02-21T15:23:46.637 回答