按符号访问操作员功能

时间:2013-02-04 21:14:30

标签: python functional-programming idioms

我需要一个函数,它将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

不能认为该字符串是安全的。我会对映射二元运算符感到满意,但如果能得到所有这些运算符,我会非常高兴。

我当前的实现手动将符号映射到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)

此解决方案在重载的运算符上失败 - add / concatsub / neg。可以添加检查以检测这些情况并检测类型或计数参数以选择正确的函数名称,但这感觉有点难看。如果我在这里没有更好的想法,那就是我要去的。

让我烦恼的是python 已经做到了。它已经知道如何将符号映射到运算符函数,但据我所知,该函数不会暴露给程序员。看起来像python中的其他所有东西,直到the pickling protocol,都暴露给程序员。那么这是哪里?或者为什么不呢?

4 个答案:

答案 0 :(得分:5)

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

例如,当您撰写2 * 3时,它不会调用mul(2, 3);它调用一些C代码来确定是使用two.__mul__three.__rmul__还是C类型等价物(插槽nb_multiplysq_repeat都等同于{{{ 1}}和__mul__)。您可以将来自C扩展模块的相同代码称为PyNumber_Multiply(two, three)。如果您查看operator.mul的来源,则它是一个完全独立的函数,可以调用相同的__rmul__

因此,没有从PyNumber_Multiply*的映射可供Python公开。

如果你想以编程方式执行此操作,我能想到的最好的方法是解析operator.mul函数的文档字符串(或者,也许是operator.c源代码)。例如:

operator

我认为这不会错过任何内容,但肯定有一些误报,例如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的运算符作为映射到{的运算符{1}}。 (确切的设置取决于你的Python版本。)随意调整正则表达式,黑名单这样的方法等等。

但是,如果你真的想写一个解析器,你可能最好为你的实际语言编写一个解析器,而不是为文档字符串编写一个解析器来生成你的语言解析器......

如果您尝试解析的语言是Python的一个子集,Python 公开内部以帮助您。请参阅ast模块了解起点。您可能仍然对callable(之类的内容感到满意,但至少应该使用operator.isCallable。例如:

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)。但是如果你想要映射到一个真正的Python _ast.Add对象......那么,它的代码也是可用的。

答案 1 :(得分: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),
    #...
}

答案 2 :(得分: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

答案 3 :(得分:0)

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

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的东西给出了错误的信息,因为没有实现符号版本