【问题标题】:Using Python dict/set to memoize algebraic expressions使用 Python dict/set 记忆代数表达式
【发布时间】:2017-10-05 20:19:05
【问题描述】:

我对将表达式存储在自引用类中很感兴趣,这些表达式可能会重复。我正在寻找一种方法的建议。我有几个想法。

例子:

a + b

现在由于加法是可交换的,如果我们认识到b + a 是同一个表达式就更好了。就我的申请而言,识别和折叠这些案例是可以的。

我一直在用这个做面条,想出了几种处理方法:

1) 使用set() 并为所有对象定义__hash____eq__,这将允许我唯一地定义表达式。一个优点是我可以很聪明并使用操作数的异或来简化检测交换情况:

class Expr(object):
    def __init__(self, op, left, right):
        self.op = op
        self.left = left
        self.right = right
    def __hash__(self):
        return hash(op) ^ hash(left) ^ hash(right)
    def __eq__(self, other):
        return self.op == other.op and \
            ((self.left == other.left and self.right == other.right) \
            or (self.left == other.right and self.right == other.left))

这样做的一个缺点是,为了测试给定的表达式是否已经存在,我需要构建一个对象来测试成员资格。如果已经在系列中,我只会丢弃它。

而且我相信,正如您所写的那样,__eq__ 函数会在您比较两棵树时引发完整的树遍历,这是您不想要的。理想情况下,记忆树的目的是简单的左/右指针值比较应该唯一地标识一个节点。

2) 使用 dict() 并使用要传递给构造函数的参数元组。为了处理交换操作,我可以为 ('+', 'a', 'b')('+', 'b', 'a') 安装密钥并将其分配给同一个对象。

key = ('+', left, right)
if key in mydict:
    expr = mydict[key]
else:
    expr = Expr('+', left, right)
    mydict[key] = expr
    mydict[('+', right, left)] = expr

return expr

我不能在这里使用 xor 方法,因为它不会产生保证唯一的密钥。

这里的事情变得有点更多有趣:

如果使用 set()/xor 方法,则可以检测关联相等。因此,(a + b) + ca + (b + c) 可以如果您选择,则可以视为相同的表达式。

【问题讨论】:

  • 您的真正问题是什么?无论如何,重要的是要知道Exprs 是否不可变......如果它们是不可变的,您可以缓存__hash__ 甚至__eq__ 的结果。另外:您可以实现__new__ 并使其创建实例Expr('+', 'a', 'b') 返回与Expr('+', 'b', 'a') 的现有实例相同的实例。拥有这些“单身人士”将允许进行身份比较以检查平等......这就是您要找的吗?
  • 你可以有一个 normalize 函数,这样每个相同的表达式最终都是相同的。例如,按字母顺序对顺序无关紧要的事物进行排序。这增加了一些计算,而存储所有表示(例如 +ab 和 +ba)会增加一些内存。
  • 不要单独存储参数;将它们存储为元组 (left, right) 用于非交换运算符和冻结集 frozenset([a, b]) 用于交换运算符。
  • 是的@Bakuriu,表达式应该是不可变的。问题或多或少是1或2更好。或者如果是其他,那也值得考虑。

标签: python dictionary set


【解决方案1】:

您可以通过缓存Expr 的实例来改进您的解决方案1,这样参数的顺序并不重要:

class CommutativeExpr(object):

    _instances = {}

    def __new__(cls, operator, left, right):
        key = (operator, frozenset([left, right]))
        try:
            return cls._instances[key]
        except KeyError:
            instance = cls._instances[key] = super().__new__(cls)
            return instance

    def __init__(self, operator, left, right):
        self.op = operator
        self.left = left
        self.right = right
        self._hash = hash((operator, frozenset([left, right])))

    def __hash__(self):
        return self._hash

    def __eq__(self, other):
        return self is other

这允许您使用身份检查来验证:

>>> CommutativeExpr('+', 'a', 'b') == CommutativeExpr('+', 'b', 'a')
True

这不支持“关联性”,但是您也可以通过正确定义 __hash__ 并让 __eq__ 进行更复杂的比较来添加该功能。


显然非交换表达式将需要不同的实现,这将是一个不同的类,或者您必须增加该类的复杂性以满足两种需求。

【讨论】:

  • 谢谢!只是出于好奇,您的示例真的需要定义 __hash__ 函数吗?由于该类没有直接用作键并且您使用的是定制元组,因此似乎不需要它?
  • @ClintOlsen leftright 可能是 CommutativeExpr 的实例,并且元组/冻结集哈希是根据元素计算的,所以是的,如果您想允许嵌套表达式,则需要它。
  • 感谢您的解释。我将您的答案标记为解决此问题,因为它给了我足够的线索。感谢有关__new__ 的线索。执行对象 interning 的方法将导致更清洁的方法。我还没有完全考虑是否可以扩展它来处理关联性,但我认为它不像交换性那么重要。
  • 在合并这种覆盖__new__ 的方法后我发现的一个问题是字典中已经存在的实例总是被初始化(当然在__init__() 中)所以我我不必要地重新计算self._hash
  • @ClintOlsen 您可以在__init__ 内部检查属性是否已设置。例如self._hash = self._hash if hasattr(self, "_hash") else hash((operator, frozenset([left,right])))
猜你喜欢
  • 2014-01-25
  • 2018-04-01
  • 2014-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-17
相关资源
最近更新 更多