Why don't callable attributes of a class become methods?

Consider the following snippet.

import types

def deff(s):
    print(f"deff called, {s=}")

lam = lambda s: print(f"lam called, {s=}")

class Clbl:
    def __call__(s):
        print(f"__call__ called, {s=}")

clbl = Clbl()

print(type(deff) == types.FunctionType)  # True
print(type(lam) == types.LambdaType)  # True
print(type(clbl) == Clbl)  # True

class A:
    deff = deff
    lam = lam
    clbl = clbl

a = A()
a.deff()  # deff called, s=<__main__.A object at 0x7f8c242d4490>
a.lam()  # lam called, s=<__main__.A object at 0x7f8c242d4490>
a.clbl()  # __call__ called, s=<__main__.Clbl object at 0x7f8c24146730>

print(a.deff is deff)  # False
print(a.lam is lam)  # False
print(a.clbl is clbl)  # True

print(type(a.deff) == types.FunctionType)  # False
print(type(a.lam) == types.LambdaType)  # False
print(type(a.clbl) == Clbl)  # True

print(type(a.deff) == types.MethodType)  # True
print(type(a.lam) == types.MethodType)  # True

Why are a.deff and a.lam methods but a.clbl not a method? Why do the expression a.deff() and a.lam() pass a as the first argument to the corresponding functions while a.clbl() doesn’t?

>Solution :

TL;DR Clbl doesn’t implement the descriptor protocol; types.FunctionType does.

All three attempts to access attributes on a fail to find instance attributes by those names, and so the lookup proceeds to the class. All three lookups succeed on class attributes, at which point it is time to see which, if any of them, define a __get__ attribute.

a.clbl does not, so you get back the actual Clbl instance bound to the class attribute.

a.deff and a.lam do (there is no significant different in between the two; types.LambdaType itself is just an alias for types.FunctionType, because lambda expressions and def statements both create values of the same type). types.FunctionType.__get__ is what returns a callable method object, which his what manages the implicit passing of an invoking instance to the underlying function.

In short, a.deff() is equivalent to

A.__dict__['deff'].__get__(a, A)()

The method object does three things:

  1. Maintains a reference __self__ to the instance a
  2. Maintains a reference __func__ to the function A.deff
  3. Implements a __call__ method that calls self.__func__ on self.__self__ and any other arguments passed to the method instance.

Leave a Reply