Search code examples
pythonpython-3.xsuperoverloadingmonkeypatching

Python3 - using super() in __eq__ methods raises RuntimeError: super(): __class__ cell not found


I am monkey patching the __eq__ method of a class. I found that the following works:

   def eq(obj, other):
       if isinstance(other, str):
          return obj.name.upper() == other.upper()
       else:
          return object.__eq__(obj, other)

This does not work:

  def eq(obj, other):
     if isinstance(other, str):
         return obj.name.upper() == other.upper()
     else:
        return super().__eq__(other)

This sometimes works, but sometimes raises and error:

def eq(obj, other):
   if isinstance(other, str):
       return obj.name.upper() == other.upper()
   else:
       return super().__eq__(self, other)

The error:

<ipython-input-128-91287536205d> in eq(obj, other)
      3         return obj.name.upper() == other.upper()
      4     else:
----> 5         return super().__eq__(self, other)
      6 
      7 

RuntimeError: super(): __class__ cell not found

Can you explain what is going on here? How do I properly replace object with super()?


Solution

  • You can't use super() without arguments in a function defined outside of a class. The __class__ cell super() relies on is only provided for functions defined in a class body. From the super() documentation:

    The zero argument form only works inside a class definition, as the compiler fills in the necessary details to correctly retrieve the class being defined, as well as accessing the current instance for ordinary methods.

    Use the 2-argument form, naming the class explicitly:

    def eq(obj, other):
       if isinstance(other, str):
           return obj.name.upper() == other.upper()
       else:
           return super(ClassYouPutThisOn, obj).__eq__(other)
    
    ClassYouPutThisOn.__eq__ = eq
    

    This requires you to explicitly name the class in the monkey patch, making it less useful for reuse.

    Instead, you can provide the required __class__ cell manually by nesting eq in another function with __class__ as a local name:

    def patch_eq(cls):
        __class__ = cls  # provide closure cell for super()
        def eq(obj, other):
           if isinstance(other, str):
               return obj.name.upper() == other.upper()
           else:
               return super().__eq__(other)
        cls.__eq__ = eq
    

    super() finds the second argument (reference to the instance), by taking the first local name from the calling frame (i.e. the first parameter passed into the function call, usually called self).

    Also see Why is Python 3.x's super() magic?

    Demo using the nested-function approach:

    >>> class Foo:
    ...     name = 'bar'
    ...     def __eq__(self, other):
    ...         return False
    ...
    >>> Foo() == 'Bar'
    False
    >>> Foo() == Foo()
    False
    >>> patch_eq(Foo)
    >>> Foo() == 'Bar'
    True
    >>> Foo() == Foo()
    False