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

Question:

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?

Asked By: CrabMan

||

Answers:

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__ passing in the required positional argument self.__self__ along with any other arguments it was called with.

As a side note, the descriptor protocol only applies to class attributes. The __get__ method, if any, of a function-valued instance attribute is ignored.

class A:
    pass

a = A()
a.foo = lambda: 3

Since foo is an instance attribute, a.foo is resolved as a.__dict__['foo'], not a.__dict__['foo'].__get__().

Answered By: chepner
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.