Unified way of introspecting callables

The documentation lists nine subtypes of callables:


3.2.8.1. User-defined functions
3.2.8.2. Instance methods
3.2.8.3. Generator functions
3.2.8.4. Coroutine functions
3.2.8.5. Asynchronous generator functions
3.2.8.6. Built-in functions
3.2.8.7. Built-in methods
3.2.8.8. Classes
3.2.8.9. Class Instances
    Instances of arbitrary classes can be made callable
    by defining a __call__() method in their class.

My concern is that all these callables have some special introspection-related attributes like func.__doc__, func.__annotations__ except the last one is different and that’s why this simple code is not correct:

def verbose_call(clb):
    print(f"LOG: going to call {clb.__name__}")
    clb()

It can fail to produce the log message, because class instances with __call__ do not have a __name__ attribute. I guess some deveplopers are not aware of this possible problem.

After special-casing the callable instance we’ll get something like this:

    if not isinstance(clb, type) and hasattr(type(clb), '__call__'):
        # case 3.2.8.9 = instances with __call__ in their class
        name = f"{type(clb).__name__}.__call__"
    else:
        name = clb.__name__
    print(f"LOG: going to call {name}")

I’d like to propose:

  • adding a function to the inspect module that returns “what will be called”. It returns the callable itself except in the mentioned special case it returns its .__call__ method. Update: also Class -> Class.__init__.

    If this doesn’t find support, I would find helpful if some information about this aspect of introspecting callables will be added to the docs.

  • stating which special attributes are common for all callables (I refer to the “what will be called” here). Something like that you can safely get __name__, __qualname__, __docs__, __module__, __annotations__ no matter what subtype of a callable you have.

Reading through this, I’m not sure if I would want to always want to get the introspection values from the __call__ method rather than the class itself? When I’m looking for a callable’s signature, it’s certainly the __call__'s that I care about. But the __name__ is always just going to be "__call__". I think that depending on the use case I’d really rather want the class’s name, qualified name or even something specific to the instance I’m calling itself. Similar things happen with __docs__, sometimes I’m looking for the __call__ docs but other times I want the class’s docs.

Also, sometimes Python’s view of “what will be called” doesn’t really line up with our conceptual view. Like, when you write SomeClass() you generally think of that calling SomeClass.__init__ and you’d want that method’s introspected properties. But really, what Python is seeing is that it’ll call type.__call__, which doesn’t really have usefully introspectable attributes.

I think that this kind of introspection is too domain-specific to really be possible to generalize like this. You can definitely come up with reasonable behaviour for every possible case, but each such set of behaviours will only really be useful for one specific use case. Because of that, I think it’s better to have stuff like this in user code rather than the standard library, even if it is a bunch of tedious boilderplate.

3 Likes

Another example showing the inconsistence.

import inspect

class AsyncF:
    async def __call__(self):
        pass

f = AsyncF()

print(f"{inspect.iscoroutinefunction(f)=}")    # False - really?
print(f"{inspect.iscoroutine(coro := f())=}")  # True - OK
coro.close()    # prevent the "never awaited" warning

Interestingly enough, from just the titles: I’m not sure lambda functions directly fall under any of those.

Lambdas are the same type of object as ‘user-defined functions’, just with

func.__name__ == "<lambda>" 
1 Like