Questions over __call__ and method-wrapper

Consider this code:

from itertools import permutations

def thing():
    print('hello')

calls = (thing,
         thing.__call__,
         thing.__call__.__call__,
         thing.__call__.__call__.__call__,
         thing.__call__.__call__.__call__.__call__)

for c in calls:
    print(id(c), c)

equalities = [c1 == c2 for c1, c2 in permutations(calls, r=2)]
print(any(equalities))

identities = [c1 is c2 for c1, c2 in permutations(calls, r=2)]
print(any(identities))

for c in calls:
    c()

Which gives us:

1953922019392 <function thing at 0x000001C6EED3F040>
1953930379216 <method-wrapper '__call__' of function object at 0x000001C6EED3F040>
1953930379120 <method-wrapper '__call__' of method-wrapper object at 0x000001C6EF537FA0>
1953930378880 <method-wrapper '__call__' of method-wrapper object at 0x000001C6EF537EE0>
1953930378688 <method-wrapper '__call__' of method-wrapper object at 0x000001C6EF537DF0>
False
False
hello
hello
hello
hello
hello

Why are these method-wrappers created? I know that they are for wrapping methods of class instances but what purpose do they serve here?

Why are the hex numbers the same for the original function and the first wrapper but the ids change?

Shouldn’t the wrapper implement __eq__ which considers id of the originally defined function?

Hi Stephen,

You ask:

“Why are these method-wrappers created?”

You are looking up attributes of objects, so the attribute lookup
machinery gets invoked.

You have a function object, thing. When you look up thing.__call__
that’s a runtime attribute lookup, which returns the method-wrapper
__call__.

That method object itself is an object, so when you look up the
__call__ attribute on that, you again get a method-wrapper.

The details are related to the descriptor protocol, which you will
find explained on the Python website. The attribute lookup machinery
doesn’t make an exception for __call__, or function objects. Any
attribute lookup goes through the same machinary.

Remember that in Python, everything is an object, including functions,
methods, and function.__call__.

As for what purpose the method-wrapper serves, without it calling
thing.__call__ would fail unless you wrote the call as:

thing.__call__(thing)

and provided the “self” parameter by hand. Remember that the __call__
method of function objects are defined on the FunctionType class, which
means that the __call__ method needs to know which instance is being
called.

You ask:

“Why are the hex numbers the same for the original function and the
first wrapper but the ids change?”

The hex number 0x000001C6EED3F040 in your example refers back to the
memory location of the original function object each time.

The object IDs are arbitrary ID numbers. The details of whether they
change or not will depend on many factors, such as the specific version
of the interpreter, whether ID numbers can get re-used or not, how
quickly they get re-used, etc. The only rules for IDs are:

  • they are integers;

  • no two different objects alive at the same time will have the same ID;

  • calling id() repeatedly on the same object will always return the same
    ID number.

In this specific case the numbers change because you are holding onto
the method objects, preventing their IDs from being recyled. Otherwise
you likely would have seen a pattern something like:

1234567890  function thing
1234567900  thing.__call__
1234567920  thing.__call__.__call__
1234567900  thing.__call__.__call__.__call__
1234567920  thing.__call__.__call__.__call__.__call__

where the IDs cycle. Another common pattern (Jython and IronPython)
would be something like:

123  function thing
124  thing.__call__
125  thing.__call__.__call__
126  thing.__call__.__call__.__call__
127  thing.__call__.__call__.__call__.__call__

that is, consecutive ID numbers with no re-use.

You ask:

“Shouldn’t the wrapper implement __eq__ which considers id of the
originally defined function?”

For what purpose?

The default __eq__ method, inherited from the base object, compares
object identity. Without some special reason to care about comparing
methods for identity, why would we change that?