在 Python 类中支持等价性(“平等”)的优雅方法

2024-12-31 08:37:00
admin
原创
92
摘要:问题描述:==编写自定义类时,通过and运算符实现等价性通常很重要!=。在 Python 中,这可以通过分别实现__eq__和__ne__特殊方法来实现。我发​​现最简单的方法是使用以下方法:class Foo: def __init__(self, item): self.item ...

问题描述:

==编写自定义类时,通过and运算符实现等价性通常很重要!=。在 Python 中,这可以通过分别实现__eq____ne__特殊方法来实现。我发​​现最简单的方法是使用以下方法:

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)

您知道有更优雅的方法吗?您知道使用上述方法比较__dict__s 有什么特别的缺点吗?

注意:需要澄清一点——当__eq____ne__未定义时,您会发现这种行为:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

也就是说,因为它确实运行了,所以a == b计算为,即身份测试(即“与是同一个对象吗?”)。False`a is bab`

__eq____ne__被定义时,你会发现这种行为(这是我们所追求的):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True

解决方案 1:

考虑这个简单的问题:

class Number:

    def __init__(self, number):
        self.number = number


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

因此,Python 默认使用对象标识符进行比较操作:

id(n1) # 140400634555856
id(n2) # 140400634555920

覆盖该__eq__函数似乎可以解决问题:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return False


n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

Python 2中,请务必记住覆盖该__ne__函数,如文档所述:

比较运算符之间没有隐含关系。 的真值x==y并不意味着 的x!=y假值。因此,在定义 时__eq__(),还应定义__ne__()以使运算符的行为符合预期。

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

在Python 3中,这不再是必要的,正如文档所述:

默认情况下,__ne__()委托给__eq__()并反转结果,除非它是NotImplemented。比较运算符之间没有其他隐含关系,例如 的真值(x<y or x==y)并不意味着x<=y

但这并不能解决我们所有的问题。让我们添加一个子类:

class SubNumber(Number):
    pass


n3 = SubNumber(1)

n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

注意: Python 2 有两种类:

  • 经典风格(或旧式)的类,不继承object且声明为class A:class A():class A(B):B经典风格类;

  • 新式类,它们继承自object并且被声明为class A(object)class A(B):其中B是新式类。Python 3 只有被声明为class A:class A(object):或 的class A(B):

对于经典风格的类,比较操作总是调用第一个操作数的方法,而对于新风格的类,它总是调用子类操作数的方法,而不管操作数的顺序如何。

因此,这里 ifNumber是一个经典风格的类:

  • n1 == n3呼叫n1.__eq__

  • n3 == n1呼叫n3.__eq__

  • n1 != n3呼叫n1.__ne__

  • n3 != n1呼叫n3.__ne__

并且如果Number是新式类:

  • 并呼叫;n1 == n3n3 == n1`n3.__eq__`

  • 并呼叫。n1 != n3n3 != n1`n3.__ne__`

为了修复Python 2 经典样式类的==and运算符的非交换性问题,当操作数类型不受支持时, and方法应该返回值。文档将该值定义为:!=`__eq____ne__NotImplemented`NotImplemented

如果数值方法和丰富的比较方法未实现所提供操作数的运算,则它们可能会返回此值。(然后,解释器将尝试反射运算或其他一些后备运算,具体取决于运算符。)其真值为真。

在这种情况下,运算符将比较操作委托给另一个操作数的反射方法。文档将反射方法定义为:

这些方法没有交换参数的版本(当左参数不支持操作但右参数支持时使用);相反,__lt__()__gt__()是彼此的反射,__le__()__ge__()是彼此的反射,和
__eq__()__ne__()它们自己的反射。

结果如下:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return NotImplemented

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    x = self.__eq__(other)
    if x is NotImplemented:
        return NotImplemented
    return not x

即使对于新式类,当操作数属于无关类型(没有继承)时需要and运算符的交换性,则返回NotImplemented值而不是返回False也是正确的做法。==`!=`

我们做到了吗?还没。我们有多少个唯一数字?

len(set([n1, n2, n3])) # 3 -- oops

集合使用对象的哈希值,默认情况下 Python 返回对象标识符的哈希值。让我们尝试覆盖它:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

最终结果如下所示(我在最后添加了一些断言以便验证):

class Number:

    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Number):
            return self.number == other.number
        return NotImplemented

    def __ne__(self, other):
        """Overrides the default implementation (unnecessary in Python 3)"""
        x = self.__eq__(other)
        if x is not NotImplemented:
            return not x
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(tuple(sorted(self.__dict__.items())))


class SubNumber(Number):
    pass


n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)

assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1

assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1

assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1

assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2

解决方案 2:

您需要小心继承:

>>> class Foo:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

更严格地检查类型,如下所示:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

除此之外,您的方法也会很有效,这就是特殊方法的用途。

解决方案 3:

您描述的方式就是我一直以来的做法。由于它是完全通用的,因此您可以随时将该功能分解为一个 mixin 类,并在需要该功能的类中继承它。

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

    def __ne__(self, other):
        return not self.__eq__(other)

class Foo(CommonEqualityMixin):

    def __init__(self, item):
        self.item = item

解决方案 4:

虽然不是直接的答案,但似乎足够相关,可以添加进去,因为它可以节省一些冗长乏味的时间。直接从文档中剪切...


functools.total_ordering(cls)

给定一个定义一个或多个丰富比较排序方法的类,此类装饰器将提供其余方法。这简化了指定所有可能的丰富比较操作所涉及的工作量:

该类必须定义__lt__()__le__()__gt__()或之一__ge__()。此外,该类还应提供一种__eq__()方法。

2.7 版本新增功能

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

解决方案 5:

您不必同时覆盖两者__eq__,而__ne__可以只覆盖__cmp__,但这会对 ==、!==、<、> 等的结果产生影响。

is测试对象身份。这意味着当 a 和 b 都持有对同一对象的引用时,a isb 将会True成立。在 python 中,您总是将对对象的引用保存在变量中,而不是实际对象中,因此本质上,对于 a is b 是否为真,它们中的对象应该位于相同的内存位置。如何以及最重要的是为什么要覆盖此行为?

编辑:我不知道__cmp__它已从 python 3 中删除,所以避免使用它。

解决方案 6:

从这个答案:https__ne__ ://stackoverflow.com/a/30676267/541136 我已经证明,虽然用术语来定义是正确的__eq__- 而不是

def __ne__(self, other):
    return not self.__eq__(other)

你应该使用:

def __ne__(self, other):
    return not self == other

解决方案 7:

我认为您要查找的两个术语是相等(==) 和身份(is)。例如:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object

解决方案 8:

“is”测试将使用内置“id()”函数测试身份,该函数本质上返回对象的内存地址,因此不可重载。

然而,在测试类的相等性时,您可能希望对测试更加严格一些,并且只比较类中的数据属性:

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

此代码将仅比较类中的非函数数据成员,并跳过任何私有数据,这通常是您想要的。对于普通旧 Python 对象,我有一个实现 init__、__str__、__repreq 的基类,因此我的 POPO 对象不会承担所有这些额外(在大多数情况下是相同的)逻辑的负担。

解决方案 9:

我喜欢使用通用类装饰器,而不是使用子类/混合器

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not self.__eq__(other)

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

用法:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b

解决方案 10:

这包含了对 Algorias 答案的评论,并通过单个属性比较对象,因为我不关心整个字典。hasattr(other, "id")必须是真的,但我知道这是因为我在构造函数中设置了它。

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

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id

解决方案 11:

支持等价性的另一种优雅方式是使用@dataclass。您的Foo示例将变为:

from dataclasses import dataclass

@dataclass
class Foo:
    item: int

就是这样!现在的行为如下:

a = Foo(1)
b = Foo(1)
print(a == b)  # True
c = Foo(2)
print(a == c)  # False

如果你的类需要提供其他实例属性,而这些属性在等价性中不起作用,那么请在中定义它们__post_init__如下所示:

from dataclasses import dataclass
from random import randint

@dataclass
class Foo:
    age: int
    name: str
    
    def __post_init__(self):
        self.rnd = randint(1, 100000)

a = Foo(38, "Helen")
b = Foo(38, "Helen")
print(a == b)  # True
print(a.rnd == b.rnd)  # False, probably ;-)

解决方案 12:

我编写了一个自定义基础,其默认实现__ne__只是简单地否定__eq__

class HasEq(object):
  """
  Mixin that provides a default implementation of ``object.__neq__`` using the subclass's implementation of ``object.__eq__``.

  This overcomes Python's deficiency of ``==`` and ``!=`` not being symmetric when overloading comparison operators
  (i.e. ``not x == y`` *does not* imply that ``x != y``), so whenever you implement
  `object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_, it is expected that you
  also implement `object.__ne__ <https://docs.python.org/2/reference/datamodel.html#object.__ne__>`_

  NOTE: in Python 3+ this is no longer necessary (see https://docs.python.org/3/reference/datamodel.html#object.__ne__)
  """

  def __ne__(self, other):
    """
    Default implementation of ``object.__ne__(self, other)``, delegating to ``self.__eq__(self, other)``.

    When overriding ``object.__eq__`` in Python, one should also override ``object.__ne__`` to ensure that
    ``not x == y`` is the same as ``x != y``
    (see `object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_ spec)

    :return: ``NotImplemented`` if ``self.__eq__(other)`` returns ``NotImplemented``, otherwise ``not self.__eq__(other)``
    """
    equal = self.__eq__(other)
    # the above result could be either True, False, or NotImplemented
    if equal is NotImplemented:
      return NotImplemented
    return not equal

如果从这个基类继承,那么只需要实现__eq__和基础。

回想起来,更好的方法可能是将其实现为装饰器。比如@functools.total_ordering

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用