【问题标题】:Access operator functions by symbol按符号访问运算符函数
【发布时间】:2013-01-19 16:27:30
【问题描述】:

我需要一个函数,它将 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,都暴露给了程序员。那么这是哪里?或者为什么不是?

【问题讨论】:

  • 我很确定您实际上不需要担心 addconcat ,除非您正在处理 C API(或使用一些实现两者的时髦 C 扩展类型插槽并做不同的事情或他们)。换句话说,除非我弄错了,否则从 Python 中,operator.add(seq1, seq2) 应该可以工作,如果没有添加插槽,则调用 concat 插槽,所以你可以使用 add
  • 另外,Python 实际上并没有将符号映射到 operator 函数;它将符号映射到像__add__ 这样的dunder 方法(实际上,即使这样也不是很准确,因为有C 扩展槽)。 operator 模块只是一堆碰巧调用相同 dunder 方法的函数。
  • 酷!你是对的,添加序列确实适用于我的解决方案。不过,否定/减法没有那么幸运。是的,我不是说 python 做了和我的代码一样的事情,我的意思是它做了我想做的事情。如果我能连接到那个系统而不是自己动手,那感觉就好了。
  • @Phil:否定是一元运算符,减法是二元运算符。 -1 是负 1,1-1 是减法。所以算上操作数,你应该没问题!
  • @Phil:除了-somevariable对于不同的类型有不同的含义;对象可以实现hooks for unary operators-somevariablesomevariable * -1相同。

标签: python functional-programming idioms


【解决方案1】:

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

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

因此,没有从 *operator.mul 的映射供 Python 公开。

如果您想以编程方式执行此操作,我能想到的最好方法是解析 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。但是,如果您想映射到实际的 Python code 对象……好吧,也可以使用该代码。

【讨论】:

  • 整洁!我之前没有玩过ast
【解决方案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),
    #...
}

【讨论】:

  • 你知道,我几乎是从这个开始的,但厌倦了输入lambda x, y: 一百万次。但是现在你再次提出它,我真的很喜欢它直接使用它映射的符号,而不是创建自己的映射到 python 运算符函数。
  • @Phil:您始终可以以编程方式生成此代码(在运行时,或使用作为“构建”过程的一部分的代码生成器步骤),而不是自己输入。事实上,我想我会这样做以避免出错的机会。 (您甚至可以结合这两个答案,并从 astoperator 遍历运算符列表,然后使用它来生成代码。)
【解决方案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 这样的东西给出的结果不正确,因为没有实现符号版本

【讨论】:

    【解决方案4】:

    您可以使用 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
    

    【讨论】:

    • 很酷的方法,非常简洁。尽管如此,仍然需要所有有效运算符的硬编码列表,这是我试图避免的。
    • 很好地使用eval(); 只要其他东西没有错误地调用make_binary_op(),就没有安全问题,因为您只传递您提供的字符。
    • 您可以将for operator in '+ - * / ^ %'.split(' '): 替换为for operator in '+-*/^%':
    • @Cyphase 不是真的,因为有些运算符有多个字符,例如//&gt;=.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-25
    • 2013-05-23
    • 1970-01-01
    • 1970-01-01
    • 2018-03-21
    • 1970-01-01
    相关资源
    最近更新 更多