__eq__ 应该比较两种不同类型的对象吗?

Posted

技术标签:

【中文标题】__eq__ 应该比较两种不同类型的对象吗?【英文标题】:Should __eq__ compare objects of two different types? 【发布时间】:2019-11-30 07:10:29 【问题描述】:

在我正在处理的问题中,有scope:name 形式的数据标识符,既是scope 又是name 字符串。 name 的不同部分由点分隔,例如 part1.part2.part3.part4.part5。在许多情况下,scope 仅等于 part1name,但并非总是如此。我正在编写的代码必须与以不同模式提供或需要标识符的不同系统一起使用。有时他们只需要像scope:name这样的完整字符串表示,在其他一些情况下,调用有两个不同的参数scopename。从其他系统接收信息时,有时会返回完整的字符串scope:name,有时会省略scope,应从name 推断,有时会返回包含scopename 的dict。

为了简化这些标识符的使用,我创建了一个类来内部管理它们,这样我就不必一遍又一遍地编写相同的转换、拆分和格式。课程非常简单。它只有两个属性(scopename,一个将字符串解析为类对象的方法,以及一些表示对象的魔术方法,特别是__str__(self)scope:name的形式返回对象,即标识符的完全限定名称 (fqn):

class DID(object):
    """Represent a data identifier."""

    def __init__(self, scope, name):
        self.scope = scope
        self.name = name

    @classmethod
    def parse(cls, s, auto_scope=False):
        """Create a DID object given its string representation.

        Parameters
        ----------
        s : str
            The string, i.e. 'scope:name', or 'name' if auto_scope is True.

        auto_scope : bool, optional
            If True, and when no scope is provided, the scope will be set to
            the projectname. Default False.

        Returns
        -------
        DID
            The DID object that represents the given fully qualified name.

        """
        if isinstance(s, basestring):
            arr = s.split(':', 2)
        else:
            raise TypeError('string expected.')

        if len(arr) == 1:
            if auto_scope:
                return cls(s.split('.', 1)[0], s)
            else:
                raise ValueError(
                    "Expecting 'scope:name' when auto_scope is False"
                )
        elif len(arr) == 2:
            return cls(*arr)
        else:
            raise ValueError("Too many ':'")

    def __repr__(self):
        return "DID(scope='0.scope', name='0.name')".format(self)

    def __str__(self):
        return u'0.scope:0.name'.format(self)

正如我所说,代码必须执行与字符串的比较并使用某些方法的字符串表示。我很想写 __eq__ 魔术方法及其对应的 __ne__。以下是刚刚__eq__的实现:

    # APPROACH 1:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.scope == other.scope and self.name == other.name
        elif isinstance(other, basestring):
            return str(self) == other
        else:
            return False

如您所见,它以一种可以相互比较的方式定义了 DID 和字符串之间的相等比较。 我的问题是这是否是一个好习惯

一方面,当other 是一个字符串时,该方法将self 转换为一个字符串,我一直在考虑显式优于隐式。您最终可能会认为您正在使用两个字符串,而 self 的情况并非如此。

另一方面,从意义的角度来看,DID 代表 fqn scope:name,并且比较与字符串的相等性是有意义的,就像比较 int 和 float 或任何比较从basetring 派生的两个对象。

我也考虑过在实现中不包括基本字符串的情况,但对我来说这更糟糕并且容易出错:

    # APPROACH 2:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.scope == other.scope and self.name == other.name
        else:
            return False

在方法 2 中,比较表示相同标识符的 DID 对象和字符串之间的相等性,返回 False。对我来说,这更容易出错。

在这种情况下有哪些最佳做法?是否应该像方法 1 中那样实现 DID 和字符串之间的比较,即使来自不同类型的对象可能被认为是相等的?即使s != DID.parse(s),我也应该使用方法2吗?不应该实现__eq____ne__ 以免产生误解吗?

【问题讨论】:

你试过1 == 1.0吗?但是请注意,__eq__ 并不是孤立存在的……这就是为什么hash(1) == hash(1.0),你不想用__eq__ 的自定义实现来打破 Liskov 原则,它会破坏子类或类似东西的散列…… 作为记录,如果你使用的是 Python 2,并且你实现了__eq__,请确保implement __ne__ in terms of __eq__; Python 3 会为你做到这一点,Python 2 不会。 鸭子类型会建议您简单地尝试属性比较,如果出现AttributeError,则回退到字符串比较。不用担心other 的确切类型。 @GiacomoAlzetta,您正式指出了我对我提出的方法的担忧。 __eq__ 不是孤立存在的。谢谢。 @ShadowRanger。我考虑到了这一点。当卡在 Python 2 中时,需要考虑到这一点。 【参考方案1】:

Python 中的一些类(但我想不出标准库中的任何内容)定义了一个在 RHS 上处理多种类型的相等运算符。一个支持这一点的常见库是 NumPy,具有:

import numpy as np

np.array(1) == 1

评估为True。总的来说,我认为我不鼓励这种事情,因为在很多极端情况下,这种行为会变得棘手。例如。请参阅 Python 3 __hash__ 方法中的文章(Python 2 中存在类似的东西,但它已停产)。在我编写类似代码的情况下,我往往会得到更接近于:

def __eq__(self, other):
    if isinstance(other, str):
        try:
            other = self.parse(str)
        except ValueError:
            return NotImplemented

    if isinstance(other, DID):
        return self.scope == other.scope and self.name == other.name

    return NotImplemented

除此之外,我建议让像这样的对象不可变,并且您有几种方法可以做到这一点。 Python 3 有很好的dataclasses,但鉴于您似乎被困在 Python 2 下,您可能会使用namedtuples,类似:

from collections import namedtuple

class DID(namedtuple('DID', ('scope', 'name'))):
    __slots__ = ()

    @classmethod
    def parse(cls, s, auto_scope=False):
       return cls('foo', 'bar')

    def __eq__(self, other):
        if isinstance(other, str):
            try:
                other = self.parse(str)
            except ValueError:
                return NotImplemented

        return super(DID, self).__eq__(other)

它免费为您提供不变性和 repr 方法,但您可能希望保留自己的 str 方法。 __slots__ 属性意味着意外分配给 obj.scopes 会失败,但您可能希望允许这种行为。

【讨论】:

注意:当您不知道如何比较类型时,我建议您返回 NotImplemented 而不是 False。这允许检查右侧的__eq__(以防它知道如何执行比较)。 @ShadowRanger 是的,这可能更好!不确定这里是否重要,我们知道 RHS 是str,因此它只会返回False,不是吗?也许如果其他人继承它? 我们对 RHS一无所知。在这种情况下,返回NotImplemented 句柄的不是 strDID(在您的第一个__eq__ 示例中比在第二个示例中需要更多)。您正在计划代码重用;在未来的某个时候,其他人可能会编写一些其他的类,应该与DID 相媲美。如果没有写出正确的__eq__,那么等式比较是否正确就看顺序了; did == didcomparable 总是返回False,而didcomparable == did 将在适当的时候返回TrueNotImplemented 修复了前者。 @TrilceAC 它还允许元组,因为它只是将所有内容传递给super.__eq__,您可以首先明确检查otherDID 的一个实例,否则返回NotImplemented。 AFAIK,文档中的“子类化”注释主要是因为初始化程序、str 和 eq 等方法不会看到这些添加的字段,除非你正式做事。它很好地继承了方法,你最终得到了一个标准类,或者我又误解了一些东西:) Point3D 不会“继承”Point 它会创建一个新的独立类,该类碰巧使用与 Point 相同的字段和一个附加字段 z!无论如何,这不是大多数人谈论继承时的意思。

以上是关于__eq__ 应该比较两种不同类型的对象吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥为 __eq__ 定义参数类型会引发 MyPy 类型错误?

使类支持比较操作

__eq__ 方法将两个自定义对象与列表进行比较

面向对象的特殊方法

Mongodb _id 在同一个集合中有两种不同的数据类型

Python面向对象编程第12篇 特殊方法之__eq__