【问题标题】:__eq__ order enforcement in PythonPython 中的 __eq__ 顺序执行
【发布时间】:2021-12-09 07:55:42
【问题描述】:

一个略长的问题,足以解释背景......

假设有一个内置的class A

class A:
    def __init__(self, a=None):
        self.a = a
    def __eq__(self, other):
        return self.a == other.a

预计这样比较:

a1, a2 = A(1), A(2)
a1 == a2  # False

出于某种原因,团队在其之上引入了一个包装器(代码示例实际上并未包装 A 以简化代码复杂性。

class WrapperA:
    def __init__(self, a=None):
        self.pa = a
    def __eq__(self, other):
        return self.pa == other.pa

同样,它应该以这种方式进行比较:

wa1, wa2 = WrapperA(1), WrapperA(2)
wa1 == wa2  # False

虽然预计会使用AWrapperA,但问题是某些代码库包含这两种用法,因此以下比较失败:

a, wa = A(), WrapperA()
wa == a  # AttributeError
a == wa  # AttributeError

一个已知的解决方案是修改__eq__:

对于wa == a

class WrapperA:
    def __init__(self, a=None):
        self.pa = a
    def __eq__(self, other):
        if isinstance(other, A):
            return self.pa == other.a
        return self.pa == other.pa

对于a == wa

class A:
    def __init__(self, a=None):
        self.a = a
    def __eq__(self, other):
        if isinstance(other, WrapperA):
            return self.a == other.pa
        return self.a == other.a

需要修改 WrapperA。对于A,既然是内置的东西,有两种解决方案:

  1. 使用 setattr 扩展 A 以支持 WrapperA。
setattr(A, '__eq__', eq_that_supports_WrapperA)
  1. 强制开发者只比较wa == a(然后不关心a == wa)。

第一个选项对于重复实现显然很难看,第二个给开发人员带来了不必要的“惊喜”。所以我的问题是,是否有一种优雅的方法可以通过内部 Python 实现将 a == wa 的任何用法替换为 wa == a

【问题讨论】:

  • 请注意,== 通常适用于 所有 类型。 A.__eq__ 要求 other 成为A 实际上是一个应该修复的错误。当它无法做出决定时,它至少应该返回NotImplemented
  • “内置事物”是指builtin type(或标准库中的其他类型)还是第三方库类型?
  • 无论你最终做什么,我建议你对AWrapperA 进行大量测试,这样当有一天有人介绍WrapperWrapperABaseA 时,你知道一切仍然有效.
  • @MisterMiyagi 例如ipaddress。假设我们要制作一个包装器(例如:IPAddress)并使其与 ipaddress.IPv4Addressipaddress.IPv6Address 具有可比性。
  • 在那种情况下我很困惑——ipaddress 类似乎在other 不兼容时正确返回NotImplemented。这意味着修改“WrapperA”就足够了。

标签: python oop


【解决方案1】:

引用MisterMiyagi在问题下的评论:

请注意,== 通常适用于所有类型。 A.__eq__ 要求 other 是 A 实际上是一个应该修复的错误。当它无法做出决定时,它至少应该返回 NotImplemented

这很重要,而不仅仅是风格问题。其实根据the documentation

当二进制(或就地)方法返回NotImplemented 时,解释器将尝试对其他类型进行反射操作。

因此,如果你只是应用 MisterMiyagi 的评论并修复 __eq__ 的逻辑,你会看到你的代码已经可以正常工作了:

class A:
    def __init__(self, a=None):
        self.a = a

    def __eq__(self, other):
        if isinstance(other, A):
            return self.a == other.a
        return NotImplemented


class WrapperA:
    def __init__(self, a=None):
        self.pa = a

    def __eq__(self, other):
        if isinstance(other, A):
            return self.pa == other.a
        elif isinstance(other, WrapperA):
            return self.pa == other.pa
        return NotImplemented

# Trying it
a = A(5)
wrap_a = WrapperA(5)

print(a == wrap_a)
print(wrap_a == a)

wrap_a.pa = 7
print(a == wrap_a)
print(wrap_a == a)
print(f'{wrap_a.pa=}')

产量:

True
True
False
False
wrap_a.pa=7

在后台,a == wrap_a 首先调用A.__eq__,然后返回NotImplemented。然后 Python 会自动尝试 WrapperA.__eq__

【讨论】:

    【解决方案2】:

    我不太喜欢这整件事,因为我认为包装一个内置函数并使用不同的属性名称会导致意想不到的事情,但无论如何,这对你有用

    import inspect
    
    
    class A:
        def __init__(self, a=None):
            self.a = a
    
        def __eq__(self, other):
            return self.a == other.a
    
    
    class WrapperA:
        def __init__(self, a=None):
            self.pa = a
    
        def __eq__(self, other):
            if isinstance(other, A):
                return self.pa == other.a
            return self.pa == other.pa
    
        def __getattribute__(self, item):
            # Figure out who tried to get the attribute
            # If the item requested was 'a', check if A's __eq__ method called us,
            # in that case return pa instead
            caller = inspect.stack()[1]
            if item == 'a' and getattr(caller, 'function') == '__eq__' and isinstance(caller.frame.f_locals.get('self'), A):
                return super(WrapperA, self).__getattribute__('pa')
            return super(WrapperA, self).__getattribute__(item)
    
    a = A(5)
    wrap_a = WrapperA(5)
    
    print(a == wrap_a)
    print(wrap_a == a)
    
    wrap_a.pa = 7
    print(a == wrap_a)
    print(wrap_a == a)
    print(f'{wrap_a.pa=}')
    

    输出:

    True
    True
    False
    False
    wrap_a.pa=7
    

    【讨论】:

      【解决方案3】:

      类似于 Ron Serruya 的回答:

      这使用__getattr__ 而不是__getattribute__,其中第一个仅在第二个引发AttributeError 或显式调用(ref) 时才被调用。这意味着如果包装器没有实现 __eq__ 并且相等应该在底层数据结构(存储在类 A 的对象中)上执行,一个工作示例由以下给出:

      class A(object):
        def __init__(self, internal_data=None):
          self._internal_data = internal_data
      
        def __eq__(self, other):
          return self._internal_data == other._internal_data
      
      class WrapperA(object):
        def __init__(self, a_object: A):
          self._a = a_object
      
        def __getattr__(self, attribute):
          if attribute != '_a':  # This is neccessary to prevent recursive calls
            return getattr(self._a, attribute)
      
      a1 = A(internal_data=1)
      a2 = A(internal_data=2)
      
      wa1 = WrapperA(a1)
      wa2 = WrapperA(a2)    
      
      print(
          a1 == a1,
          a1 == a2,
          wa1 == wa1,
          a1 == wa1,
          a2 == wa2,
          wa1 == a1)
      
      >>> True False True True True True
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-11-01
        • 2019-09-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多