Search code examples
pythonpython-2.7static-methodsclass-method

Class variable reference to function changes to instancemethod


I'm trying to call an external function via a class variable. The following is a simplification of my real code:

def func(arg):
    print(arg)

class MyClass(object):
    func_ref = None

    @classmethod
    def setUpClass(cls):
        #MyClass.func_ref = func
        cls.func_ref = func

    @staticmethod
    def func_override(arg):
        print("override printing arg...")
        MyClass.func_ref(arg)

if __name__ == "__main__":
    print(type(func))
    print(type(MyClass.func_ref))
    MyClass.setUpClass()
    print(type(MyClass.func_ref))
    MyClass.func_override("hello!")

The above code produces the following output:

[~]$ python tmp.py
<type 'function'>
<type 'NoneType'>
<type 'instancemethod'>
override printing arg...
Traceback (most recent call last):
  File "tmp.py", line 20, in <module>
    MyClass.func_override("hello!")
TypeError: func_override() takes exactly 2 arguments (1 given)

The situation seems to be unchanged if I use MyClass in place of cls within the classmethod setUpClass().

I would expect the type of MyClass.func_ref to be function after the assignment in setUpClass() which explains the TypeError I get when I try to call it. Why is the type of func_ref being changed to instancemethod when the value I assigned to it is of type function?

This only seems to be an issue in Python 2. Python 3 behaves as I would expect.

How do I get calls to the static method MyClass.func_override() to call func()?

UPDATE

I was able to get the above to work by applying the following patch:

@@ -14,7 +14,7 @@ class MyClass(object):
     def func_override(arg):
         print("override printing arg...")
         func(arg)
-        MyClass.func_ref.__func__(arg)
+        MyClass.func_ref(arg)

 if __name__ == "__main__":
     print(type(func))

While the above works, its not at all clear to me why I needed to do this. I still don't understand why the type of func_ref ends up an instancemethod when I assigned to it a value of type function.


Solution

  • Just put the function through a staticmethod as follows:

        @classmethod
        def setUpClass(cls):
            #MyClass.func_ref = func
            cls.func_ref = staticmethod(func)
    

    There's no need to play with @-based decorators in this case as you want to modify how the method is bound to MyClass, not the general definition of func.

    Why is this necessary? Because, when you assign a method to class, Python assumes you'll want to refer to an instance (via self) or the class (via cls). self, unlike this in JS, is only a naming convention, so when it sees arg it assumes it got an instance, but you passed a string in your call.

    So, as as Python cares, you might have as well have written def func(self):. Which is why the message says unbound method func() must be called with MyClass 👉instance👈 as first argument.

    staticmethod means, "please leave this alone and don't assume an instance or a class in the first variable".

    You can even dispense with the setUpClass entirely:

    class MyClass(object):
        func_ref = staticmethod(func)
    

    BTW: In 2021, 16 months past EOL, Python 2.7 has all the subtle fagrance of moldy gym socks. Except less safe, virologically-speaking.