4

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.

awm129
  • 305
  • 2
  • 11

2 Answers2

1

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.

JL Peyret
  • 10,917
  • 2
  • 54
  • 73
  • Thank you! The "Python assumes you'll want..." is exactly what I was looking for. I'd up 2.7's fragrance to moldy gym socks left in a hot car. But even with the stench, 2.7 is still mission critical for some large and expensive systems. Thanks again! – awm129 Apr 29 '21 at 19:07
0

When func_ref is called, it's expecting a self argument, just like any other normal (instance) class method (see this question and answers for discussions why). You can either add a self argument to func or make func a static method:

@staticmethod
def func(arg):
    print(arg)

>>> MyClass.setUpClass()
>>> MyClass.func_override("hello!")
override printing arg...
hello!

Note that in either case func is now not normally callable as a regular function:

>>> func('what does this do?')
TypeError: 'staticmethod' object is not callable

If you need func to be usable as a regular function, you can wrap it with another, qualifying function and use the wrapper in MyClass:

def func(arg):
    print(arg)

@staticmethod
def func_wrapper(arg):
    func(arg)

class MyClass(object):
    @classmethod
    def setUpClass(cls):
        cls.func_ref = func_wrapper  # use wrapper function

>>> MyClass.setUpClass()
>>> MyClass.func_override("success!")
override printing arg...
success!
Woodford
  • 3,746
  • 1
  • 15
  • 29
  • Thanks! I understand that `func_ref` is now expecting a `self` argument, but why? I assigned it the value of a "normal" function - not a method. I want `func` to remain a "norrmal' function un-associated with a class (as opposed to any kind of method, static or otherwise) – awm129 Apr 28 '21 at 18:07
  • @awm129 Updated to address your comment – Woodford Apr 28 '21 at 18:18
  • Thanks again. The wrapper could work, but I really want to call `func` via my class variable. I figured out something that works, but it isn't at all clear to me why it works. – awm129 Apr 28 '21 at 18:47