santasmic santasmic - 11 months ago 71
Python Question

Functions, Callable Objects, and how both are created in Python

I'm wondering about the more intricate differences between functions and callable objects. For example, if you do:

def foo1():
return 2 + 4

class foo2:
def __call__():
return 2 + 4

import sys
sys.getsizeof(foo1) # returns 136
sys.getsizeof(foo2) # returns 1016

There's clearly a big difference between functions and callable objects. However, I can't find a lot of documentation on what is going on behind the scenes. I know functions are first-class objects, but I also know that classes have a lot more going on than your regular functions. class foo2 is created with a metaclass, type().

My questions then, are these:

  1. When you create a function "def foo1():", how does this differ from
    the process of defining a class with a metaclass? Is there a version of type() but for functions, a metafunction?

  2. Say someone wanted to write their own metafunction (the real reason behind this), would it be better to just use decorators, or maybe a metaclass that makes callable classes? What advantages would either offer (a metaclass that makes callable classes seems clunky)?

  3. Is the only purpose behind having a callable object to have a function that can store info in it as well?

Answer Source

Functions are also callable objects:

>>> foo1.__call__
<method-wrapper '__call__' of function object at 0x105bafd90>
>>> callable(foo1)

But a class needs to keep track of more information; it doesn't matter here that you gave it a __call__ method. Any class is bigger than a function:

>>> import sys
>>> def foo1():
...    return 2 + 4
>>> class foo3:
...    pass
>>> sys.getsizeof(foo1)
>>> sys.getsizeof(foo3)

A function object is a distinct object type:

>>> type(foo1)
<class 'function'>

and is reasonably compact because the meat is not actually in the function object but in other objects referenced by the function object:

>>> sys.getsizeof(foo1.__code__)
>>> sys.getsizeof(foo1.__dict__)

And that's it really; different types of objects have different sizes because they track different things or use composition to store stuff in other objects.

You can use the type(foo1) return value (or types.FunctionType, which is the same object) to produce new function objects if you so desire:

>>> import types
>>> types.FunctionType(foo1.__code__, globals(), 'somename')
<function foo1 at 0x105fbc510>

which is basically what the interpreter does whenever a def function(..): ... statement is being executed.

Use __call__ to make custom classes callable when that makes sense to your API. The enum.Enum() class is callable, for example, specifically because using the call syntax gives you a syntax distinct from subscription, which was used for other purposes.