Python __eq__ 中是否需要传递性?

Is there a need for transitivity in Python __eq__?

我正在使用自定义 __eq__ 实现自己的 class。我想 return True 对于数学意义上不“相等”但模糊“匹配”的事物。

但是,这会导致数学意义上的传递性丧失,即 a == b && b ==c,而 a 可能不等于 c

问题:Python 是否依赖于 __eq__ 是可传递的?我正在尝试做的事情是否会破坏事物,或者只要我自己小心不要假设传递性,就可以这样做吗?

用例

我想将电话号码相互匹配,而这些电话号码可能是国际格式,也可能仅供国内使用(未指定国家代码)。如果没有指定国家代码,我希望一个数字等于一个数字,但如果指定,它应该只等于具有相同国家代码的数字,或者没有一个。

所以:

编辑:我不需要散列(因此不会实现它),所以至少让生活更轻松。

没有 MUST 而是 SHOULD 关系,用于比较与通常理解的关系一致。 Python 明确地不强制执行此操作,并且 float 是一种内置类型,由于 float("nan").

而具有不同的行为

Expressions: Value comparisons

[…]
User-defined classes that customize their comparison behavior should follow some consistency rules, if possible:

  • […]
  • Comparison should be symmetric. In other words, the following expressions should have the same result:
    • x == y and y == x
    • x != y and y != x
    • x < y and y > x
    • x <= y and y >= x
  • Comparison should be transitive. The following (non-exhaustive) examples illustrate that:
    • x > y and y > z implies x > z
    • x < y and y <= z implies x < z

Python does not enforce these consistency rules. In fact, the not-a-number values are an example for not following these rules.

不过,请记住,例外情况极其罕见,很容易被忽略:例如,大多数人会将 float 视为具有完全秩序。使用不常见的比较关系会严重增加维护工作量。


通过运算符对“模糊匹配”进行建模的规范方法是 subsetsubsequencecontainment 使用 非对称 运算符。

  • setfrozenset支持>>=等表示一个集合包含另一个集合的所有值。
    >>> a, b = {1, 5, 6, 8}, {5, 6}
    >>> a >= a, a >= b, b >= a
    (True, True, False)
    
  • strbytes 支持 in 表示子序列被覆盖。
    >>> a, b = "+31 6 12345678", "6 12345678"
    >>> a in b, b in a
    (False, True)
    
  • rangeipaddress 网络支持 in 来表示涵盖特定项目。
    >>> IPv4Address('192.0.2.6') in IPv4Network('192.0.2.0/28')
    True
    

值得注意的是,虽然这些运算符可能是可传递的,但它们不是对称的。例如,a >= b and c >= b 并不意味着 b >= c,因此也不意味着 a >= c,反之亦然。

实际上,可以将“没有国家代码的号码”建模为同一号码的“有国家代码的号码”的超集。这意味着 06 12345678 >= +31 6 1234567806 12345678 >= +49 6 12345678 但反之则不然。为了进行对称比较,可以使用 a >= b or b >= a 而不是 a == b

__eq__ 方法应该是可传递的;至少字典是这么认为的。

class A:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        for element in self.values:
            if element is other:
                return True
        return False

    def __hash__(self):
        return 0

    def __repr__(self):
        return self.name

x, y, z = A('x'), A('y'), A('z')
x.values = [x,y]
y.values = [x,y,z]
z.values = [y,z]

print(x == y)
--> True

print (y == z)
--> True

print(x == z)
--> False

print({**{x:1},**{y:2, z: 3}})
--> {x: 3}

print({**{x:1},**{z:3, y:2}})
--> {x: 1, z: 2}

{**{x:1},**{y: 2, z:3}} 是两个字典的并集。没有人期望字典在更新后删除键。

print(z in {**{x:1},**{y:2, z: 3}})
--> False

通过更改联合中的顺序,您甚至可以获得不同大小的词典:

print(len({**{x:1},**{y:2, z: 3}}))
--> 1

print(len({**{x:1},**{z:3, y:2}}))
--> 2