8

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()?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
oz123
  • 27,559
  • 27
  • 125
  • 187
  • There is no difference between your 'does not work' and 'sometimes works' examples, other than indentation. Both only 'sometimes' work as long as the `else` branch is not reached. – Martijn Pieters May 04 '17 at 09:34
  • Note that `super(...).__eq__` is a *bound method*, so the first argument (usually named `self`) is automatically passed in from the second argument to `super(cls, instance)`. You only need to pass in `other`: `super(...).__eq__(other)`. – Martijn Pieters May 04 '17 at 09:39
  • I've encountered this error while creating `__new__` outside of the class (dynamically, for tests purposes, you really don't need this kind of magic in actual code), using 2-arguments `super` call like `super(metaclass, cls).__new__(cls)` with `metaclass` almost always being just `type` solved the issue – Azat Ibrakov Feb 04 '20 at 05:14

1 Answers1

21

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
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343