pythonpython-3.xdynamic-languages

Is it possible to convert a python function into a class?


I am new to Python coming from C++ background and this is the first time I am seeing a language which contains nothing but objects. I have just learned that class and functions are also just objects. So, is there a way to convert the following function to a class?

In [1]: def somefnc(a, b):
...:     return a+b
...: 

I have first tried assigning the __call__ variable to None to take away the "callable nature" from the function. But as you can see, the __call__ was successfully replaced by None but this didn't cause the function to stop adding numbers when called, though, somefnc.__call__(1,3) was working before assigning somefnc.__call__ to None

In [2]: somefnc.__dict__ 
Out[2]: {}

In [3]: somefnc.__call__
Out[3]: <method-wrapper '__call__' of function object at 0x7f282e8ff7b8>

In [4]: somefnc.__call__ = None

In [5]: x = somefnc(1, 2)

In [6]: print(x)
3

In [7]: somefnc.__call__

In [8]: print(somefnc.__call__(1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
ipython-input-8-407663da97ca     
in <module>()
----> 1 print(somefnc.__call__(1, 2))

TypeError: 'NoneType' object is not callable

In [9]: print (somefnc(1,2))
3

In [10]: 

I am not doing this for developing purpose here, so claiming this to be a bad practice will not make any sense. I am just trying to understand Python very well. Of course, for development purpose, I could rather create a class than to convert a function to one!

After robbing the function off its ability to add two numbers, I am thinking of assigning a valid function to the attribute somefnc.__init__ and some members by modifying somefun.__dict__, to convert it to a class.


Solution

  • In Python functions are instances of the function class. So I'll give you a general answer about any class and instance.

    In [10]: class Test:
        ...:     def __getitem__(self, i):
        ...:         return i
        ...:     
    
    In [11]: t = Test()
    
    In [12]: t[0]
    Out[12]: 0
    
    In [13]: t.__getitem__(0)
    Out[13]: 0
    
    In [14]: t.__getitem__ = None
    
    In [15]: t[0]
    Out[15]: 0
    
    In [16]: t.__getitem__(0)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-16-c72f91d2bfbc> in <module>()
    ----> 1 t.__getitem__(0)
    
    TypeError: 'NoneType' object is not callable
    

    In Python all special methods (the ones with double underscores in the prefix and the postfix) are accessed from the class when triggered via operators, not from the instance.

    In [17]: class Test2:
        ...:     def test(self, i):
        ...:         return i
        ...:     
    
    In [18]: t = Test2()
    
    In [19]: t.test(1)
    Out[19]: 1
    
    In [20]: t.test = None
    
    In [20]: t.test(1)
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-22-261b43cb55fe> in <module>()
    ----> 1 t.test(1)
    
    TypeError: 'NoneType' object is not callable
    

    All methods are accessed via the instance first, when accessed by name. The difference is due to different search mechanics. When you access a method/attribute by name, you invoke __getattribute__ which will first search in the instance's namespace by default. When you trigger a method via operators, __getattribute__ is not invoked. You can see it in the disassembly.

    In [22] import dis
    
    In [23]: def test():
        ...:     return Test()[0]
        ...:     
    
    In [24]: dis.dis(test)
      2           0 LOAD_GLOBAL              0 (Test)
                  3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
                  6 LOAD_CONST               1 (0)
                  9 BINARY_SUBSCR
                 10 RETURN_VALUE
    
    In [25]: def test2():
        ...:     return Test().__getitem__(0)
        ...: 
    
    In [26]: dis.dis(test2)
      2           0 LOAD_GLOBAL              0 (Test)
                  3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
                  6 LOAD_ATTR                1 (__getitem__)
                  9 LOAD_CONST               1 (0)
                 12 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
                 15 RETURN_VALUE
    

    As you can see, there is no LOAD_ATTR in the first case. The [] operator is assembled as a special virtual-machine instruction BINARY_SUBSCR.